C++类和对象

发布时间:2023年12月19日

面向过程与面向对象

C语言是面向过程的,关注的是做一件事情的需要的步骤有哪些,通过一系列函数之间的调用配合来实现解决问题

例如打游戏,需要 拿出电脑,开机,联网,登录,进入游戏这几个步骤

C++是基于面向对象的,关注的是解决这一个问题参与的对象,依靠对象之间的交互来完成问题的解决

例如打游戏,对象就是电脑和人,对于电脑这个对象,内部可能有一些机理,比如联网,登录,对于人也有一些机理,例如肌肉运动之类的,这就类似于类中的函数

C语言中的结构体只能定义变量,但是在C++中,结构体中还可以定义函数,那么其实对于C++来说结构体也是一个类,而在C++中更常用class表示真正的类

类的定义

class ClassName
{
	void fun()
	{
	
	}
	int a;
};//注意分号

class为定义类的关键字,ClassName是类的名字,{}是类的主体,之中是类的成员,包括成员变量和成员函数

类的两种定义方式

  1. 声明和定义全都在类中定义,成员函数可能会被当成内联函数处理
class student
{
	public:
		void init()
		{
			cin>>_name>>_sex>>_age;
		}
	public:
		char* _name;
		char* _sex;
		int _age;
}
  1. 类声明在头文件中,成员函数放在源文件中,成员函数需要加类名
class student
{
	public:
		void init();
	public:
		char* _name;
		char* _sex;
		int _age;
}
#include<student.h>
void student::init()
{
	cin>>_name>>_sex>>_age;
}

一般情况下尽量使用第二种,在类中定义的内部变量为了和临时变量能够区分清楚需要加入一定的前缀和后缀

类的访问限定符及封装

访问限定符

C++实现封装的方式:用类将对象的属性和方法相结合,通过访问权限选择性的将接口提供给外部用户使用
在这里插入图片描述

注意

  1. public修饰的成员可以在类外被直接访问
  2. protected和private修饰的成员在类外不能被直接访问,两者类似
  3. 访问权限作用域从该访问限定符的位置开始直到下个访问限定符出现位置,或者到类的结束
  4. class的默认访问权限为private,struct的为public

封装

这里主要讲解面向对象的三大特性(封装,继承,多态)之一,封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
说人话就是别问咋实现的,给你这东西你会用就行。
封装本质上是一种管理,让用户更方便使用类。在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

类的作用域

类本身就是一个作用域,类中的所有成员都在类的作用域中,要在类外定义成员,例如变量或函数,时,需要使用作用域操作符指明属于哪个域

class student
{
	public:
		void init();
	public:
		char* _name;
		char* _sex;
		int _age;
}

void student::init()
{
	cin>>_name>>_sex>>_age;
}

类的实例化

用类创建对象的过程,成为类的实例化,说人话就是用这个类创建了一个变量,例如 int a ,就是int实例化的一个过程。

类对象模型

类对象的大小

class A
{
	public:
		void PrintA()
		{
		   cout<<_a<<endl;
		}
	private:
		char _a;
};

虽然看上去这个函数在类中,但实际上函数所占的空间并不在类中,因为如果是在类中的话,实例化相当数量的对象时,内存的占用会大大提高,为了避免这种事情的发生,会把成员变量放在一起,而成员函数放在公共代码区,这样即使实例化多个对象,成员函数始终只存在一个而且方便调用。所以类和对象的大小实际上跟结构体大小的计算方法时完全相同的

一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。

结构体内存的对其规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

this 指针

this 指针

这里为了引出this指针,先定义一个日期类

class Date
{ 
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
 		_day = day;
 	}
	void Print()
 	{
 	cout <<_year<< "-" <<_month << "-"<<_day<<endl;
 	}
private:
	int _year;     // 年
	int _month;    // 月
	int _day;      // 日
};
int main()
{
	Date d1, d2;
	d1.Init(2023,8,2);
	d2.Init(2023,8,3);
 	d1.Print();
 	d2.Print();
 	return 0;
}

那么对于这个类Date,在函数中并没对不同对象进行区分,那么当d1调用Init函数和d2调用Init函数时,编译器如何对这两个对象进行区分呢

实际上在对象中有一个隐藏的this指针,当函数对对象进行操作的时候就可以通过这个指针找到对象的成员变量

this 指针的特性

  1. this指针的类型是 *const,也就是不能再成员函数中给this指针赋值,this指针所指向的空间不能改变
  2. this指针本质上是一个形参,因此只能在成员函数的内部进行使用,而且在对象调用函数时,是将对象的地址作为实参传递给this形参,因此对象本身并不存储this指针
  3. this是隐藏的指针形参,所以并不需要用户传递,否则会报错,一般情况由编译器通过ecx寄存器自动传递

在这里插入图片描述

类的六个默认成员函数

如果类中什么都没有,就成为空类,会占一个字节的空间,来表示这里有一个类
但其实在空类中,编译器仍然会自动生成六个默认成员函数
在这里插入图片描述

构造函数

我们之前写过一个Date类

class Date
{ 
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
 		_day = day;
 	}
	void Print()
 	{
 	cout <<_year<< "-" <<_month << "-"<<_day<<endl;
 	}
private:
	int _year;     // 年
	int _month;    // 月
	int _day;      // 日
};
int main()
{
	Date d1, d2;
	d1.Init(2023,8,2);
	d2.Init(2023,8,3);
 	d1.Print();
 	d2.Print();
 	return 0;
}

我们可以自己写一个公用的初始化函数Init(),但是,只要我们实例化一个类,就要进行一次调用,因此C++就会自动生成一个默认成员函数,名字与类名相同,称为默认构造函数,在每次实例化类的时候会自动进行调用,以保证每个数据成员都由一个合适的初始值
一般来说这个构造函数如何进行初始化是需要自己编写的,好在不需要自己进行调用,省了不少时间

下面是使用的演示,具体内容我们会在接下来的内容中讲到

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;     // 年
	int _month;    // 月
	int _day;      // 日
};
int main()
{
	Date d1(2023, 8, 2);
	Date d2(2023, 8, 3);

	return 0;
}

我们可以在调试信息中看到d1,d2中成员变量的值
在这里插入图片描述
同理我们可以在输出窗口中加一些提示信息看到构造函数的调用情况
在这里插入图片描述

特性

构造函数实际上是特殊的成员函数,而且构造函数本身的任务并不是创建变量而是初始化变量

  1. 函数名与类名相同
  2. 无返回值
  3. 实例化时编译器会自动调用相应的构造函数
  4. 构造函数可以重载,也就是说可以默认初始值,或者手动传参,都是很方便的操作,需要注意的是,在使用全缺省的时候,不需要加空括号,因为会和函数的声明混淆
  5. 如果用户没有显示定义构造函数,那么编译器会自动生成一个无参的默认构造函数,一旦定义,编译器也就不会自动生成了
  6. 实际上在自动初始化的时候,对于内置类型,包括整形,浮点型,指针等,编译器是不会自动初始化的,而对于自定义类型例如class类,编译器会自动调用他所定义的构造函数,所以对于最基本的类就只用初始化一次就足够了
  7. 在C++11中,对于内置类型不支持初始化的缺陷进行了修补,在内置基本类型的声明时,可以给一个默认值,如果在初始化的时候没有给定,就会自动使用默认值

析构函数

概念

同样的,与初始化相对应,还有变量的销毁,我们称之为析构函数,但要注意的是,销毁的其实是对象中的成员变量等,对象的销毁是由编译器自身完成的

特性

  1. 析构函数的函数名是在类名前加~
  2. 无参数无返回值类型
  3. 一个类只用一个析构函数,若用户没有定义,编译器会自动生成,而且析构函数不能重载
  4. C++会自动调用析构函数、
  5. 对于内置类型析构函数不会处理,对于自定义类型,析构函数会调用他自己的析构函数
  6. 在没有进行内存申请时,析构函数可以不写,否则一定要写,不然会造成内存泄漏

拷贝构造函数

概念

用于拷贝和赋值自定义类型的值
拷贝构造函数只有单个形参,该形参是对本自定义类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

特征

  1. 拷贝构造函数是构造函数的重载形式,因此函数名也和类名一样
  2. 拷贝构造函数只有一个,必须是对自定义类的引用,用直接传值回引发无穷递归,因为传值本质上还是实参向形参拷贝
  3. 若没有显示定义,编译器会生成默认的拷贝构造函数,这种拷贝构造函数只能按照字节进行拷贝,这种拷贝叫做浅拷贝,或值拷贝,存在许多弊端,例如在对栈进行拷贝的时候,只能将地址拷贝,并不能拷贝地址的内容,此时又因为存在析构函数,将对象析构两次,就会引发报错
  4. 拷贝构造函数的调用场景
    1. 使用已存在对象创建新的对象
    2. 函数参数类型为自定义类型对象
    3. 函数返回值类型为自定义类型对象

后面两种情况实际上是因为在传值和返回值时会自动拷贝

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
比如对自定义类型对象的比较大小,加减,在写这类的函数时也应当考虑其实际含义是否有价值
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:

  • 不能通过连接其他符号来创建新的操作符:比operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。

const 对象

在许多情况下,我们并不想让调用的成员函数改变对应的成员变量,而在成员函数定义或声明时又无法显式写出this指针来限制,因此c++提供了这样一种语法来限制this指针所指向的对象不能被修改

class Date
{
public:
	void print() const
	{
		cout<<_year<<' '<<_month<<' '<<_day<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

可以理解为下面的第二种形式,其中的this所指向的内容不能被改变

void print() const;
void print(const Date* this);

在实际的操作中,const对象和非const对象都存在读和写的需求。而在只读的成员函数中则可以只写const成员函数,因为非const对象也可以调用const成员函数,这里实际上是权限的缩小,那么也可以得到如下结论,const对象不能调用非const成员函数,因为权限扩大了。const成员函数不能调用其他非const成员函数,非const成员函数可以调用其他const成员函数

取地址操作符的重载

对于取地址操作符,我们需要定义const成员函数和非const函数的重载,在一般情况下直接使用默认生成的取地址重载即可,当然也可以自己写一个函数,达到保密的效果

构造函数之二

构造函数体的赋值

在创建对象时,编译器会调用构造函数赋予对象的成员变量的初始值,例如我们之前写过的日期类

class Date
{
public:
Date(int year, int month, int day)
{
     _year = year;
     _month = month;
     _day = day;
}
private:
	int _year;
	int _month;
	int _day;
};

但实际上这并不算初始化,而属于初赋值,在实际的使用过程中,我们会遇到一些无法赋值的情况,例如const成员变量,引用变量,自定义类型成员且没有默认构造函数,由此c++发展出了初始化列表

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
还是上面的日期类,我们可以这样初始化

class Date
{
public:
Date(int year, int month, int day)
	:_year(year)
	,_month(month)
	,_day(day)
{}
private:
	int _year;
	int _month;
	int _day;
};
  1. 每个成员变量在初始化列表中只能出现一次
  2. 包含上面的特殊情况时必须放在初始化列表中初始化
  3. 尽量使用初始化列表进行初始化,因为自定义类型成员变量一定会先使用初始化列表初始化
  4. 初始化的顺序按照构造函数的声明顺序进行初始化

static 成员

概念

static的类成员被称为类的静态成员,包含静态成员变量和静态成员函数,此外在使用静态成员变量时一定要在类外进行初始化,后续将介绍静态成员变量和静态成员函数特性

特性

  1. 静态成员属于整个类,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义(注意区分定义与声明的概念),定义不添加static,声明时添加
  3. 类的静态成员可以用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,因此也不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private的限制

注意: 区分static静态成员和const成员

友元

友元是一种突破类的限制的方法,为了提供便利,破坏封装,提高耦合度,通常不建议封装,同样的,友元也分为友元函数和友元类

友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字,也就是说这个函数可以直接访问类中的成员,十分方便

说明

  1. 友元函数可以访问私有成员,但不是类的成员函数
  2. 友元函数不能用const修饰、
  3. 友元函数可以在类定义的任何地方声明,不受访问限定符的限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

友元类

友元类的所有成员函数都是另一个类的友元函数,都可以直接访问私有成员

说明

  1. 友元关系是单向的,不可以交换
  2. 友元关系是不可传递的
  3. 友元关系不能继承,后续介绍

内部类

概念: 类定义在另一个类的内部,则成为内部类,内部类是独立的一个类,不属于外部类,也不能通过外部类访问内部类的成员

注意:内部类是外部类的友元类,内部类可以通过外部类的对象参数访问外部类的所有成员,但外部类不是内部类的友元

特性:

  1. 内部类可以定义在外部类的public、protected、private
  2. 内部类可以直接访问外部类中的static成员,不需要外部类的对象或者类名
  3. sizeof(外部类) = 外部类成员的大小,不包含任何内部类成员

匿名对象

匿名对象与c语言中的匿名结构体类似,可以直接在函数中使用,因为匿名对象不需要取名字,匿名对象和实名对象的基本操作都还在,比如构造与析构,只是没有名字,其次匿名对象的生命周期只有这一行。

使用

class A()
{
public:
	A(int a = 0)
		:_a = a;
	{
		cout<<"A(int a = 0)"<<endl;
	}
	~A()
	{
		cout<<"~A()"<<endl;
	}
	int Fun(int input = x)
	{
		return _n + x;
	}
private:
	int _a;
	static int _n;
}
int A::_n = 5;
int main()
{
	A a1;//这里是实名对象,对象名为a1
	A();//这里就是一个匿名对象
	A().Fun(5);//如果想直接调用函数可以利用匿名对象
	return 0;
}

拷贝对象的优化

在传参和返回值时,编译器会做一些优化,减少对象的拷贝,大多数编译器都会优化

例如要将返回的对象拷贝给一个新的对象,此时就会直接拷贝给新的对象,而不会利用临时对象再去拷贝,这样可以节省大量资源。

文章来源:https://blog.csdn.net/ye_yumo/article/details/135068818
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。