6_C++类和对象(超详细)

类和对象


面向对象和面向过程

面向对象相当于是关注对象之间的关系,比如在点餐系统中更加注意商家、用户、骑手之间的联系

面向过程相当于是更加注意流程函数的实现,比如点餐系统中更加注意点餐、送餐、选餐等等流程函数的实现

常见的一些语言:

C:面向过程

C++:基于面向对象的语言

Java:纯面向对象的语言


C++中的类


访问限定符

C++中对于类的访问限定符存在以下三类:private、public、private。

C++是基于面向对象的语言,C++兼容C语言中的struct语法,同时将struct进行了升级(升级为类)

即使这样C++对于struct类class类还是有一定的区别的,

class默认类的访问限定符为privatestruct默认访问限定符为public

所以我们推荐使用class


类的定义

类的定义:

  • 在类中定义:在类中的定义,可能会被当做内联函数处理。

  • 分离定义:分离的定义

面向对象的三大特征:封装、继承、多态

封装的本质是一种管理


这里就想到了域,

关于域主要影响到的是访问权限范围,

那什么影响到的是声明周期呢?

生命周期要看存放的区域

也就是存储位置,这里会影响到生命周期


类的实例化

什么是类的实例化?

首先我们应该先知道什么是类?类我们可以理解为建筑物的图纸,有了图纸就一定有房子吗?也就是说有了类就一定有对象吗?不一定,所以我们把通过类定义对象时来开辟空间的过程叫做类的实例化。

类成员变量是声明还是定义呢?

声明(开辟空间是定义,否则就是声明)

sizeof(类)?

sizeof在计算类型大小的时候是通过类型大小来计算的,所以并不需要一定要实例化才可以计算所占空间的大小


类和对象的存储方式

首先先从汇编去看

class student
{
public:
	void print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "_num" << _num << endl;
	}
private:
	char _name[20];
	int _age;
	int _num;
};

int main()
{
	student s1;
	s1.print();
	student s2;
	s2.print();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q7XnQ3Uj-1691424493839)(https://dhrs-oss.oss-cn-beijing.aliyuncs.com/img/202307292118553.png)]

这里可以看到调用的函数是同一个

也就是我们可以这么理解对象和函数并无关联

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flatnrpV-1691424493840)(https://dhrs-oss.oss-cn-beijing.aliyuncs.com/img/202307292134276.png)]

也就是说编译器在编译链接时就去根据函数名称去公共代码区找到对应函数的地址


那么以下函数的调用能否正常运行

A* ptr = nullptr;
ptr->func();
//func函数内没有使用类成员变量

结论是可以正常运行的,因为类成员变量和类成员函数的存储方式使得对象即使未实例化也可以调用类成员函数


这里值得注意的是类成员变量依旧遵循内存对齐规则

关于类的大小(占位)

有成员变量:遵循内存对齐规则

无成员变量: (这里会开辟1byte的占位,不存储实际数据,仅标示对象存在)

  • 有成员函数:1byte
  • 无成员函数:1byte

this指针

在调用类成员函数的时候编译器会在第一个变量前加上this(可以在函数内部使用,但不能显示的传递和接收)

this指针存在哪里?

栈区,因为this指针属于一个形参(可能会被优化到寄存器,目的是为了提高this指针的访问效率,这个具体是否优化取决于编译器。)


六个默认成员函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e4JaLlB3-1691424493840)(https://dhrs-oss.oss-cn-beijing.aliyuncs.com/img/202307302210366.png)]

什么是默认成员函数?

就是即使是一个空类,编辑器都会帮你生成这些默认成员函数,即不需要用户显示写的类成员函数就是默认成员函数。


构造函数

构造函数是用来初始化的,不是用来定义的

构造函数可以理解为函数名和类名相同、无返回值、可以重载,用于初始化类成员变量

如果没有显示定义构造函数,则编译器会默认生成一个无参的默认构造函数

一旦用户显示定义构造函数,则编译器不再自动生成

构造函数可以重载,对象实例化时自动调用


1.构造函数

构造函数可以分为无参的构造函数、带参的构造函数、用户自定义生成的构造函数

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

构造函数可以重载

class A
{
public:
	A() {}//无参的构造函数
	A(int a)//带参数的构造函数
	{
		_a = a;
	}
private:
	int _a;
};
int main()
{
	A a1;
	A a2(1);
	//在调用无参数的构造函数创建对象的时候不需要加括号,否则就成了函数声明
	//如下会产生警告:warning C4930: “A a3(void)”: 未调用原型函数(是否是有意用变量定义的?)
	A a3();
	return 0;
}

image-20230731205806023


image-20230731210034739


所以这里就会涉及到一个问题,关于自定义默认构造函数,这里其实是可以涉及到函数缺省的。

class A
{
public:
	A(int a = 0)//用户自定义的默认构造函数
	{
		_a = a;
	}
private:
	int _a;
};
int main()
{
	A a1(1);
	return 0;
}

2. 默认构造函数到底有什么用?

在我们不进行自定义构造函数的时候,编译器会默认生成构造函数,但是默认生成的构造函数有什么用呢?

对内置类型不做处理,对自定义类型调用自定义类型的构造函数

例子:

class Time
{
public:
	Time()//构造函数
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
private:
	int _year;
	int _month;
	int _day;
	Time _t;  //自定义类型
};

int main()
{
	Date d1;
	return 0;
}

//运行截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jSroWo1D-1691424493841)(C:\Users\86176\AppData\Roaming\Typora\typora-user-images\image-20230804105959331.png)]

由此可见编译器会自动生成Date的构造函数,并且会调用自定义类型Time的构造函。


3. 类内置类型成员变量声明时可以给默认值

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
	}
private:
	int _hour = 0;
	int _minute = 0;
	int _second = 0;//注意这里不是赋值,可以理解为缺省值
};

析构函数

析构函数与构造函数相反,析构函数不是完成的对象销毁(局部对象的销毁是由编译器完成的),而是在对象销毁时候(出了对象的作用域)自动调用,目的是为了清理资源(资源释放).

1. 析构函数
  • 默认析构函数的主要特性是内置类型不做处理,自定义类型的成员调用自定义类型的析构函数

  • 析构函数的函数名是在类名称前加上~

  • 析构函数没有参数和返回值

  • 析构函数不能重载,如果用户没有显示写出则会自动生成

2.析构函数的调用

析构函数是在对象生命周期结束的时候自动调用的

class Date
{
public:
	~Date()
	{
		cout << "~Date" << endl;
	}
private:
	int _day;
};

int main()
{
	Date d1;
	return 0;
}

析构函数的调用

可以看出编译器会自动调用Date的析构函数.


4. 默认析构函数作用

默认析构函数和默认构造函数一样,对内置类型不做处理,对自定义类型,调用自定义类型的析构函数.

class Time
{
public:
	~Time()
	{
		cout << "~Time" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
private:
	int _day;
	Time _t;
};

int main()
{
	Date d1;
	return 0;
}

析构函数

在这里,main函数中并没有调用Time类为什么最后还会调用Time呢?

原因是因为Time类型的类在Date中被使用,在我们调用Date类的时候,编译器会自动调用Time

内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可

_tTime类对象,所以d1销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁 .

注意:

  • 要析构哪个类就调用哪个类对应的析构函数

  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。


拷贝构造函数

1.拷贝构造函数
  • 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型
    对象创建新对象时由编译器自动调用。 (即是为了创建一个与已知对象一样的对象出来)
  • 拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数不得传值传参

拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发
无穷递归调用。

所以为什么不能传值传参呢?

因为传值传参会产生临时对象的拷贝,而自定义类型临时对象的拷贝又需要调用对应的拷贝构造函数,以此类推,周而复始会引发无穷递归.

image-20230805113851253

所以正确写法见下

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)//传引用传参
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	return 0;
}

3.默认构造函数

编译器生成的默认构造函数可以用吗?

这里就涉及到值拷贝的问题,也就是深浅拷贝的问题.

浅拷贝

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

class Date
{
public:
	Date(int year = 2000,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void DPrint()
	{
		cout << this->_year << ":" << this->_month << ":" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(1, 1, 1);
	Date d2(d1);
	d1.DPrint();
	d2.DPrint();
	return 0;
}

image-20230805143800767

这样便是浅拷贝,即按字节为单位,逐个拷贝.但是这样会产生一个问题见下:

深拷贝

深拷贝:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};

int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

image-20230805145156508

如上,如果依旧采用编译器自动生成的拷贝函数的时候会引发浅拷贝,即会造成两个对象s1s2指向同一块区域,造成越界访问,并且程序会报错,报错的原因如下

程序首先为s1开辟空间,接着当s2拷贝构造时候,编译器自动生成默认拷贝构造函数,此时为浅拷贝,按字节依次拷贝,就会造成s2指向了事先为s1开辟的空间,当程序退出的时候程序先销毁s2再销毁s1,在销毁s1的时候,s2已经将这块空间销毁了,s1并不知道,就会尝试再次销毁,一块空间不能被释放两次,进而造成了程序的崩溃

结论

类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 造成错误

4. 拷贝构造函数典型调用场景
  1. 使用已存在对象创建新对象
  2. 函数参数类型为类类型对象
  3. 函数返回值类型为类类型对象
//以下为举例(以下代码共调用几次默认构造和拷贝构造呢?)
class Date
{
public:
	Date(int year, int minute, int day)
	{
		cout << "Date(int year, int minute, int day)" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date temp(d);
	return temp;
}
int main()
{
	Date d1(2022, 1, 13);
	Test(d1);
	return 0;
}

image-20230805151406965

分别是:

Date Test(Date d)//传值传参调用一次拷贝构造
{
	Date temp(d);//拷贝构造调用一次拷贝构造
	return temp;//传值返回调用一次拷贝构造
}
int main()
{
	Date d1(2022, 1, 13);//调用一次构造函数
	Test(d1);
	return 0;
}
结论
  • 为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用
5. 如何使用拷贝构造
Date d1(1, 1, 1);
Date d2(d1);
Date d3 = d1;

构造函数和析构函数的调用

析构函数和构造函数的调用同样遵循"先进后出",即先定义的后销毁,后定义的先销毁.

class A
{
public :
	A(int a = 1)
	{
		_a = a;
		cout << "A()" << this->_a << "-----" << this << endl;
	}
	~A()
	{
		cout << "~A()" << this->_a << "-----" << this << endl;
	}
private:
	int _a;
};

static A a5(5);

void func2()
{
	A a6(6);
}

void func()
{
	A a7(7);
	func2();
}

int main()
{
	static A a1(1);
	A a2(2);
	A a3(3);
	static A a4(4);
	func();
	return 0;
}

image-20230805183247125

如图所示为构造和析构的顺序


如果涉及到传参了呢?

class A
{
public :
	A(int a = 1)
	{
		_a = a;
		cout << "A()" << this->_a << "-----" << this << endl;
	}
	A(const A& a)
	{
		_a = a._a;
		cout << "A(const A& a)" << this->_a << "-----" << this << endl;
	}
	~A()
	{
		cout << "~A()" << this->_a << "-----" << this << endl;
	}
private:
	int _a;
};
A func1()
{
	static A a2(2);
	return a2;
}
A& func2()
{
	static A a3(3);
	return a3;
}
int main()
{
	A a1(1);
	func1();
	func2();
	return 0;
}

image-20230805203136002

这里需要注意传引用和传值不同!


运算符重载

内置类型可以直接使用运算符进行运算,但是自定义类型不支持直接使用运算符进行运算

所以C++提出了运算符重载:

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

1. 运算符重载
  • 函数名字为:关键字operator后面接需要重载的运算符符号。

  • 不能通过连接其他符号来创建新的操作符

  • 重载操作符必须有一个类类型参数

  • 用于内置类型的运算符,其含义不能改变

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

  • .* :: sizeof ?: .注意以上5个运算符不能重载。

2. 封装运算符重载函数
class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

//private:
	int _year;
	int _month;
	int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year &&
		d1._month == d2._month &&
		d1._day == d2._day;
}

int main()
{
	Date d1(2000, 1, 1);
	Date d2(2000, 1, 1);
	Date d3(2001, 1, 1);
	cout << (d1 == d2) << endl;
	cout << (d1 == d3) << endl;
	return 0;
}

上述代码虽然可以实现运算符重载但是,在类外定义的函数需要访问私有变量,会破坏类的封装性。

  • 在类里面写一个函数来返回类私有成员变量的值,在类外调用这些函数来接收这个值
  • 友元
  • 写在类的内部(需要注意的是放在类内部的时候,类里会含有this指针 ,即函数的参数只需一个)

运算符重载放在类内部,因为存在this指针,所以只需要一个参数。

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year &&
			_month == d._month &&
			_day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

3. 赋值运算符重载

赋值运算符重载和运算符重载相同,但需要注意的是遇到d1 = d2 = d3情况下的连续赋值,即我们需要将赋值运算符重载函数的返回值也定义为类,如下:

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator=(const Date& date)
	{
		if (this != &date)//判断是否是本身的赋值,那就没有意义
		{
			this->_year = date._year;
			this->_month = date._month;
			this->_day = date._day;
		}
		return *this;//返回值为了实现连续赋值
	}
	void print()
	{
		cout << _year << ":" << _month << ":" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2000, 1, 1);
	Date d2(2001, 1, 2);
	Date d3(1111, 1, 1);
	d2.print();
	d3 = d2 = d1;
	d2.print();
	d3.print();
	return 0;
}

image-20230806103131896

赋值运算符只能重载成类的成员函数不能重载成全局函数

  • 赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的
    赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的
    成员函数。

用户若没有显示写赋值运算符重载编译器也会自动生成一个,自动生成的函数与拷贝构造函数相同,都是按照字节拷贝,并且遇到自定义类型还会调用自定义类型的函数,但是与拷贝构造函数相同的是这里完成的也是浅拷贝,所以一旦涉及到资源的开辟管理,我们必须自行完成赋值运算符的定义。

因为如果涉及到资源的管理,一旦进行浅拷贝,则之前的数据则会丢失,并且还造成了指向同一块空间

image-20230806105419526


4. 前置++和后置++

前置++后置++都是一样的++符号,那么我们应该怎么区分呢?或者应该怎么告诉编译器我们到底要调用的哪个函数呢?

  • 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载

  • C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

  • 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1而temp是临时对象,因此只能以值的方式返回,不能返回引用

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator++()//前置++
	{
		_day += 1;
		return *this;//this对象在使用后不会销毁所以使用this引用提高效率
	}
	Date operator++(int)//后置++
	{
		Date temp(*this);
		_day += 1;
		return temp;//因为是后置++,需要先使用后++也就是需要我们返回之前的值即需要我们存放这个临时对象
	}
	void print()
	{
		cout << _year << ":" << _month << ":" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2000, 1, 1);
	d1++;
	d1.print();
	(d1++).print();
	(++d1).print();
	d1.print();
	return 0;
}

image-20230806142140613

这里需要我们注意的是:

  • 前置++

    在返回的时候需要我们进行返回这个结果成员以便我们使用(d1++).print()但是如果前置++没有返回值的时候就会造成无法这样使用

  • 后置++

    后置++需要注意的是后置++的逻辑是先使用后++,但是当我们调用这个函数的时候,成员的值已经被改变了。所以我们在后置++返回值的时候需要对原来的值进行保存,返回这个原来的值,然后再说++。

5. cout运算符重载

cout运算符究竟是什么呢?

click on an element for detailed information

可以看到这个图,在C++中coutcin都是对应的类来提供的。但是库里面提供的只能支持内置类型的重载使用(原理是函数名的修饰规则),对于自定义类型我们需要自行完成。

image-20230806192026057


class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2000, 1, 1);
	d1.operator<<(cout);
	d1 << cout;
	return 0;
}

image-20230806192420118

这里我们在调用的时候为什么要这么写呢?

因为在类成员函数,会有一个this指针强行占据第一位,所以只能这么调用

就没有什么其他的解决方法改为cout << d1了吗?

将该函数改为友元或者全局,即将函数的第一个参数改为cout

class Date
{
public:
	friend ostream& operator<<(ostream& out, const Date& date);
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
inline ostream& operator<<(ostream& out,const Date& date)
{
	out << date._year << "年" << date._month << "月" << date._day << "日" << endl;
	return out;
}


int main()
{
	Date d1(2000, 1, 1);
	Date d2(2001, 1, 1);
	cout << d1 << d2;
	return 0;
}

image-20230806193302131

这样就将该函数改为友元函数并且可以在类外面访问私有成员变量。

  • 至于这里为什么要将返回类型改为ostream&,目的是为了实现连续的成员打印,类似于赋值运算符的重载。
  • 因为可能会涉及到频繁调用,所以这里我们将其设置为内联函数
  • 类里面添加friend是构成友元函数,随后会提到

const修饰的成员函数

权限的放大和缩小

在调用类成员函数的时候可能会出现以下情况:

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year &&
			_month == d._month &&
			_day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2000, 1, 1);
	const Date d2(2000, 1, 1);
	d1.operator==(d2);
	d2.operator==(d1);
	return 0;
}

image-20230806194155274

造成这种情况的原因如下

bool operator==(Date* const this ,const Date& d);

可以看出来这里的this指针被const修饰,const修饰指针有以下两种:

const Date* this;//这里是修饰的被指针指向的内容

Date* const this;//这里修饰的是指针

对于这里强制隐式添加的是指针,但是我们的d2是被const修饰的对象,传递给函数的时候会发生权限的放大,造成不能调用的问题。所以为了可以完成调用,但是这里的this指针是隐式添加的,我们不能显示补充,所以可以用const来修饰成员函数,就是在成员函数后面添加const

bool operator==(const Date& d)const
{
    return _year == d._year &&
        _month == d._month &&
        _day == d._day;
}

这样就可以使用const修饰的类对象完成调用了。


取地址和const取地址的函数重载

这两个函数一般不需要我们自行实现,编译器自动生成的就足够我们使用了。

class Date
{
public:
    Date* operator&()
    {
    	return this;
    }
    const Date* operator&()const
    {
    	return this;
    }
private:
    int _year;
    int _month;
    int _day;
};

当我们不希望用户获取到我们的成员地址,或者不希望获取真实值的时候可以这么做


初始化列表

1. 关于构造函数

虽然构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

关于默认构造函数的定义内置类型不做处理,自定义类型调用它的构造函数


这里就类似于是对默认构造函数的一个补充,但是如果我们要在一个类里面初始化另一个类呢?

以下是可以想到的解决办法:

  • 友元
  • 写个带参的构造函数,然后再利用赋值重载(下面会写一下)
  • 初始化列表
  • ……

2. 利用赋值重载

关于写个带参的构造函数,然后再赋值重载的意思如下:

class Time
{
public:
	friend ostream& operator<<(ostream& out, const Time& time);
	Time(int hour = 0, int minute = 0, int second = 0)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	friend ostream& operator<<(ostream& out, const Date& date);
	Date(int year = 0, int month = 0, int day = 0, int hour = 0, int minute = 0, int second = 0)
	{
		cout << _t << endl;
		_year = year;
		_month = month;
		_day = day;
		Time t(hour, minute, second);//调用拷贝构造生成临时对象t
		_t = t;//将t赋值给_t(因为无法在Time类外访问私有成员变量)
	}
private:
	int _year;
	int _month;
	int _day;

	Time _t;
};

inline ostream& operator<<(ostream& out, const Time& time)
{
	out << time._hour << "时" << time._minute << "分" << time._second << "秒";
	return out;
}
inline ostream& operator<<(ostream& out, const Date& date)
{
	out << date._year << "年" << date._month << "月" << date._day << "日";
	out << date._t << endl;
	return out;
}

int main()
{
	Date d1(2000, 11, 12, 11, 12, 15);
	cout << d1;
	return 0;
}

image-20230807024543893

这里就可以看到,在进了Date的构造函数,Date类中的构造函数就已经调用了Time类的构造函数初始化为0、0、0了。所以我们才会打印出来0、0、0,然后进行的是构造一个临时对象t(11、12、15),然后将t赋值给Date类中的_t,也不失为一种方法。


3. 初始化列表

关于初始化列表我们该怎么写呢?

class Time
{
public:
	friend ostream& operator<<(ostream& out, const Time& time);
	Time(int hour = 0, int minute = 0, int second = 0)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};


class Date
{
public:
	friend ostream& operator<<(ostream& out, const Date& date);
	Date(int year = 0, int month = 0, int day = 0, int hour = 0, int minute = 0, int second = 0)
		:_year(year)
		,_month(month)
		,_day(day)
		,_t(hour,minute,second)
	{
	}
private:
	int _year;
	int _month;
	int _day;

	Time _t;
};

inline ostream& operator<<(ostream& out, const Time& time)
{
	out << time._hour << "时" << time._minute << "分" << time._second << "秒";
	return out;
}
inline ostream& operator<<(ostream& out, const Date& date)
{
	out << date._year << "年" << date._month << "月" << date._day << "日";
	out << date._t << endl;
	return out;
}

int main()
{
	Date d1(2000, 11, 12, 11, 12, 15);
	cout << d1;
	return 0;
}

以上便是初始化列表完成初始化


注意

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

    • const成员变量

      关于为什么const成员必须通过初始化列表进行初始化

      首先const定义的变量或者对象只有一次初始化的机会,随后便不可以做任何修改

      如果我们不通过初始化列表进行初始化的话,从第2点利用赋值函数重载中就可以验证出来,在进入第一个类的时候已经被调用默认构造进行初始化了,那我们就无法做出修改,只可以通过在调用默认构造函数之前调用初始化列表进行初始化。

    • 引用成员变量

      引用和const一样都必须要在定义的时候进行初始化

    • 自定义类型成员(且该类没有默认构造函数时)

  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

    • 根据这个可以看一下代码:
    class A
    {
    public:
    	A(int a)
    		:_a1(a)
    		, _a2(_a1)
    	{}
    	void Print() 
     {
    		cout << _a1 << " " << _a2 << endl;
    	}
    private:
    	int _a2;
    	int _a1;
    };
    int main() {
    	A aa(1);
    	aa.Print();
     return 0;
    }
    

    以上代码输出的结果是

    image-20230807115830501

    这就是因为初始化列表的顺序是依照类中的声明顺序来的


explicit和匿名对象

1. explicit关键字(阻止隐式类型的转换)

这里我们先看以下两个调用:

 class A
{
public:
	A(int a = 0)
	{
		_a = a;
		cout << "A(int a = 0)" << endl;
	}
	A(const A& a)
	{
		this->_a = a._a;
		cout << "A(const A& a)" << endl;
	}
	void Print() {
		cout << _a << endl;
	}
private:
	int _a;
};
int main() {
	A a1(2023);//调用构造
	A a2 = 2023;//调用构造再调用拷贝构造(实际上经过编译器的优化为直接调用构造)
	return 0;
}

image-20230807213609369

A a2 = 2023;中实际上中间会发生隐式类型的转换

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用

如果我们不希望发生这种隐式类型的转换,可以用 explicit关键字来修饰构造函数


2. 匿名对象
const A& a3 = 2023;//这个实际上引用的是中间产生的临时变量

这个临时变量(对象)我们称之为匿名对象。

匿名对象有什么作用呢?

匿名对象可以帮助我们不创建特定对象的情况下调用函数

class A
{
public:
	void Print() {
		cout << _a << endl;
	}
private:
	int _a = 0;
};
int main() {
	A().Print();
	return 0;
}

这里就是利用匿名对象在不创建特定对象的情况下调用类成员函数


静态成员变量&&函数

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰成员函数,称之为静态成员函数

静态成员也是成员,依旧收到访问限定符的限制。

1. 静态成员变量

注意

  1. 静态成员变量一定要在类外面进行初始化

  2. 静态成员变量不能在声明的时候给缺省值

    这里要知道在C++11中类成员变量在定义的时候添加缺省值是提供给初始化列表用的

  3. 生命周期在整个程序运行期间,存放在静态区。

  4. 静态成员变量属于整个类和类的所有对象。

  5. 静态成员变量也存在共有和私有

静态成员变量的声明、定义、使用

class A
{
public:
	void Print() {
		cout << _a << endl;
	}
	int get_a()
	{
		this->_a++;
		return this->_a;//在类成员函数里面可以访问私有静态成员变量_a
	}
	static int _b;//声明(公有)
private:
	static int _a;//声明(私有)
};
int A::_a = 10;//定义
int A::_b = 20;//定义
int main() {
	A a1;
	A a2;
	//私有静态成员变量
	cout << a1.get_a() << endl;
	cout << a2.get_a() << endl;

	//公有静态成员变量
	cout << A::_b << endl;
	cout << a1._b++ << endl;
	cout << a2._b++ << endl;
	return 0;
}

image-20230807221917125


2.静态成员函数

对于_a这种非公开的静态成员变量我们需要单独编写get_a()来实现,但是get_a()是类成员函数,需要我们创建对象来调用,即使是匿名对象也是对象,这里我们可以通过静态成员函数来解决。

注意

  1. 静态成员函数没有this指针
    • 没有this指针那么调用的时候就不需要通过类对象来调用
    • 没有this指针那么就不可以调用非静态的函数(非静态的函数需要指针)

静态成员函数的声明、定义、使用

class A
{
public:
	void Print() {
		cout << _a << endl;
	}
	static int get_a()
	{
		return _a;//没有this指针,可以直接访问私有静态成员变量
	}
private:
	static int _a;
};
int A::_a = 10;

int main() {
	cout << A::get_a() << endl;//不需要对象也可以直接调用
	return 0;
}

友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多
用 。

友元是单向的,例如A是B的友元,则可以理解为B的就是A的,A的还是A的。

1. 友元函数

友元函数前面已经用过了就是把函数声明放在类里面的任意位置,并且在声明前添加friend即该函数就成为该类的友元。

注意

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

详细使用代码见cout运算符重载


2. 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。


例如在Date类和Time类中,Date类无法直接访问Time类中的成员,则可以将Date 类变为Time类的友元类即可。

class Time
{
	friend class Date; //友元类
public:
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, int hour = 1, int minute = 1, int second = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;//直接访问Time的私有成员变量
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

int main()
{
	Date d1(2,2,2,2,2,2);
	return 0;
}

注意

  1. 友元关系是单向的,不具有交换性。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  2. 友元关系不能传递
    如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  3. 友元关系不能继承,

内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意

  1. B定义在A里面,则B受到A类域限制,访问限定符的限制。
  2. B定义在A里面,B天生就是A的友元。
  3. 内部类可以定义在外部类的public、protected、private都是可以的。
  4. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  5. sizeof(外部类)=外部类,和内部类没有任何关系。

内部类的使用以及解释:

class A
{
public:
	A(int a = 1)
		:_a(a)
	{}
	class B
	{
	public:
		B(int b = 1)
			:_b(b)
		{}
		void f(const A& a)
		{
			cout << k << endl;
			cout << a._a << endl;
		}
	private:
		int _b;
	};
private:
	int _a;
	static int k;
};
int A::k = 3;

int main()
{
	cout << sizeof(A) << endl; // 4(A的大小与B无关)
	A a(1);
	A::B b(1);//通过限定标示符进行实例化
	b.f(a);//在f中访问A中的私有成员(友元)
	A::B().f(1);//匿名对象
	return 0;
}

image-20230807231141244


补充

在不同的编译器下,如果连续的调用拷贝构造可能会被不同程度的优化。

class W
{
public:
	W(int x = 0)
	{
		cout << "W()" << endl;
	}

	W(const W& w)
	{
		cout << "W(const W& w)" << endl;
	}

	W& operator=(const W& w)
	{
		cout << "W& operator=(const W& w)" << endl;
		return *this;
	}

	~W()
	{
		cout << "~W()" << endl;
	}
};

void f1(W w)
{

}

void f2(const W& w)
{

}

int main()
{
	W w1;//1次构造
	cout << endl;
	f1(w1);//1次拷贝构造
	cout << endl;
	f2(w1);//不需要调用构造或者拷贝构造
	cout << endl;
	f1(W()); // 1次构造+1次拷贝构造--优化---直接构造
	cout << endl;
	W w2 = 1;//1次构造(临时变量)+1次拷贝构造---优化--直接构造

	return 0;
}

image-20230807232031284


总结:


类和对象

  • C++中的类是面向对象的
  • 访问限定符存在以下三类:private、public、private
  • 写一个类相当于是画了建筑物的图纸,只有类的实例化才是以图纸为基础进行定义
  • 类成员函数都被编译器隐式添加了this指针
  • C++11中打了补丁,类成员变量在声明时可以给缺省值,缺省值服务于初始化列表
  • 类中的对象也遵循“先进后出”原则,即先定义的后析构,后定义的先析构
  • 为了便于const修饰的对象调用类成员函数,应将需要的函数用const来修饰
  • explicit关键字是为了阻止隐式类型的转换
  • 匿名对象可以帮助我们在不创建特定对象的情况下调用类成员函数
  • 编译器会对连续的拷贝构造进行优化,不同的环境优化程度不同

构造函数

  • 构造函数可以理解为函数名和类名相同、无返回值、可以重载,用于初始化类成员变量
  • 一般我们会自行写全缺省的构造函数
  • 如果没有显示定义构造函数,则编译器会默认生成一个无参的默认构造函数
  • 一旦用户显示定义构造函数,则编译器不再自动生成
  • 构造函数可以重载,对象实例化时自动调用
  • 编译器默认生成的构造函数,对内置类型不做处理,对自定义类型调用自定义类型的构造函数
  • 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

析构函数

  • 默认析构函数的主要特性是内置类型不做处理,自定义类型的成员调用自定义类型的析构函数

  • 析构函数的函数名是在类名称前加上~

  • 析构函数没有参数和返回值

  • 析构函数不能重载,如果用户没有显示写出则会自动生成

  • 析构函数的目的是为了完成资源的释放,由编译器在对象生命周期结束的时候自动调用

  • 涉及到资源的释放问题时,一定要主动写析构函数,否则会造成资源的泄露


拷贝构造

  • 拷贝构造是构造的另一个重载形式
  • 拷贝构造的目的是为了创建一个与已知对象相同的对象
  • 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发
    无穷递归调用。
  • 涉及资源的申请时一定要自行完成深拷贝构造的编写

运算符重载

  • 函数名字为:返回值 operator要重载的运算符(参数)

  • 不能通过连接其他符号来创建新的操作符

  • 重载操作符必须有一个类类型的参数

  • 用于内置类型的运算符,其含义不能改变

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

  • .* :: sizeof ?: .注意以上5个运算符不能重载。

  • 运算符重载函数尽量封装到类中,尽可能的避免破坏类的封装

  • 赋值运算符重载

    1. 编写赋值运算符重载时要注意连续赋值的情况,即需要注意返回值
    2. 赋值运算符重载只能是类成员函数
  • 前置++和后置++

    1. C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

    2. 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1而temp是临时对象,因此只能以值的方式返回,不能返回引用

    3. 前置++

      在返回的时候需要我们进行返回这个结果成员以便我们使用(d1++).print()但是如果前置++没有返回值的时候就会造成无法这样使用

    4. 后置++

      后置++需要注意的是后置++的逻辑是先使用后++,但是当我们调用这个函数的时候,成员的值已经被改变了。所以我们在后置++返回值的时候需要对原来的值进行保存,返回这个原来的值,然后再说++。

  • cout运算符重载

    1. cout运算符的重载返回类型应该改为ostream&,目的是为了实现连续的成员打印,类似于赋值运算符的重载。
    2. 因为可能会涉及到频繁调用,所以这里我们将其设置为内联函数
    3. 应将cout的重载设为友元来便于我们使用(因为放在类成员函数中会有第一个this指针的影响)
  • 其他类型的运算符重载可以通过互相调用来实现

    例如“>”可以通过调用“<”运算符……


初始化列表

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  • 类中包含以下成员,必须放在初始化列表位置进行初始化:

    1. const成员变量
    2. 引用成员变量
    3. 自定义类型成员(且该类没有默认构造函数时)
  • 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关


静态成员

  • 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰成员函数,称之为静态成员函数
  • 静态成员也是成员,依旧收到访问限定符的限制。
  • 静态成员变量
    1. 静态成员变量一定要在类外面进行初始化
    2. 静态成员变量不能在声明的时候给缺省值
    3. 生命周期在整个程序运行期间,存放在静态区。
    4. 静态成员变量属于整个类和类的所有对象。
    5. 静态成员变量也存在共有和私有
  • 静态成员函数
    1. 静态成员函数没有this指针
    2. 没有this指针那么调用的时候就不需要通过类对象来调用
    3. 没有this指针那么就不可以调用非静态的函数(非静态的函数需要指针)

友元

  • 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多
    用 。
  • 友元关系是单向的,不具有交换性
  • 友元关系不能传递
  • 友元关系不能继承
  • 友元函数
    1. 友元函数前面已经用过了就是把函数声明放在类里面的任意位置,并且在声明前添加friend即该函数就成为该类的友元。
    2. 友元函数可访问类的私有和保护成员,但不是类的成员函数
    3. 友元函数不能用const修饰
    4. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
    5. 一个函数可以是多个类的友元函数
    6. 友元函数的调用与普通函数的调用原理相同
  • 友元类
    1. 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

内部类

  • 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
  • B定义在A里面
    1. 则B受到A类域限制,访问限定符的限制。
    2. B定义在A里面,B天生就是A的友元。
  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值