C++面向对象二..

24.const成员

const成员:被const修饰的成员变量、非静态成员函数

class Car {
const int m_price;
public:
	// const修饰的非静态成员函数中 const必须放在参数列表的后面
	void run() const {
		cout << "Car::run()" << endl;
	}
};

const成员变量:
必须进行初始化操作(类内部初始化 有别于static成员变量的类外部初始化) 可以在声明的时候进行初始化操作
非static的const成员变量还可以在初始化列表中完成初始化操作(静态成员变量可以不创建对象 如果不创建对象的话 就无法调用构造函数对const成员变量进行初始化操作了)

class Car {
const static int ms_price = 100;
public:
	void run() const {
		cout << "Car::run()" << endl;
	}
};
class Car {
const int m_price;
public:
	Car(int price) : m_price(price) {

	}
	int getPrice() {
		return m_price;
	}
	void run() const {
		cout << "Car::run()" << endl;
	}
};

const成员函数(非静态)
const关键字必须写在参数列表的后面 如果函数的声明和实现是分离的 必须两者都要标明const关键字

class Car {
const int m_price = 100;
public:
	void run() const;
};
void Car::run() const {
	cout << "Car::run()" << endl;
}

内部不能修改非静态的成员变量

class Car {
const int m_price = 100;
int static m_wheel;
int m_people;
public:
	void run() const {
		m_wheel = 8;// ok
		m_people = 6;// error
	};
};

内部只能调用const成员函数、static成员函数
为什么内部不能够调用普通的非静态函数呢 因为这类的函数内部可以修改非静态成员变量 但是在const函数中 是不能够修改非静态的成员变量的 而const函数以及static函数内部都是不可以修改非静态成员变量的

class Car {
public:
	// const函数
	void speak() const {
		cout << "Car::speak()" << endl;
	}
	// static函数
	static void jump() {
		cout << "Car::jump()" << endl;
	}
	// 非静态、非const函数
	void sing() {
		cout << "Car::sing()" << endl;
	}
	// const函数
	void run() const {
		// 可以访问const函数以及static函数
		speak();// ok
		jump();// ok
		// 不可以访问除了上述两种以外的函数
		sing();// error
	}
};

非const成员函数是可以调用const成员函数的

class Car {
public:
	// 非const函数
	void speak() {
		cout << "Car::speak()" << endl;
		run();
	}
	// const函数
	void run() const {
		cout << "Car::run()" << endl;
	}
};

const成员函数和非const成员函数会构成重载关系(指的是函数名相同 参数列表也相同的情景下 一般的重载都是函数名相同 参数列表不同)
那这样不会引发歧义吗 答案是不会的 因为他会进行区分 当我们创建的是非const对象 去调用重载函数时 调用的是非const函数 而当我们创建const对象 去调用重载函数时 调用的则是const函数
那么为什么会有这样的区别呢 原因在于当我们使用const去修饰对象的时候 是不可以修改内部的非静态成员变量的(原因在于非静态成员变量属于对象内部 而静态成员变量不属于对象内部) 如果我们去调用重载函数时调用的是非const函数的话 那么在其内部是允许修改成员变量的 互相矛盾 所以const对象调用重载函数时 调用的是const函数
总结一下:非const对象会优先调用非const函数(因为非const对象也可以调用const函数)、const对象只能调用const函数、static函数(const对象规定不可以修改非静态成员变量 而const成员函数和static成员函数都遵循这个规定)

class Car {
public:
	static void speak() {
		cout << "Car::speak()" << endl;
	}
	void run() {
		cout << "Car::run() no const" << endl;
	}
	void run() const {
		cout << "Car::run() const" << endl;
	}
};
int main() {
	Car car1;
	car1.run();
	const Car car2;
	car2.run();
	car2.speak();
	getchar();
	return 0;
}

25.引用类型成员

引用类型的成员必须进行初始化(但是不考虑static的情况 因为这种情况很少见 不是说不行)
他可以在声明的同时初始化
也可以在初始化列表中完成初始化操作

class Car {
	int age;
	int& m_price = age;
};
class Car {
	int& m_price;
public:
	Car(int& price) : m_price(price) {

	}
};

26.拷贝构造函数

拷贝构造函数就是构造函数的一种
当我们利用已经存在的对象去创建一个新对象(类似于拷贝) 就会调用新对象的拷贝构造函数进行初始化操作
拷贝构造函数的格式是固定的 接收一个const引用作为参数
这个案例中 我们的拷贝操作是内置于拷贝构造函数中的 当我们根据一个已存在的对象创建新对象的时候 就会去调用这个拷贝构造函数完成所有成员变量的拷贝操作

class Car {
	int m_price;
	int m_length;
public:
	Car(int price = 0, int length = 0) : m_price(price), m_length(length) {
		cout << "Car::Car(int price = 0, int length = 0)" << endl;
	}
	Car(const Car& car) {
		cout << "Car::Car(const Car& car)" << endl;
		m_price = car.m_price;
		m_length = car.m_length;
	}
	void display() {
		cout << "price = " << m_price << ", length = " << m_length << endl;;
	}
};
int main() {
	Car car1(100, 200);
	car1.display();
	Car car2(car1);
	car2.display();
	getchar();
	return 0;
}

拷贝构造函数也可以使用初始化列表完成对象的初始化工作

class Car {
	int m_price;
	int m_length;
public:
	Car(int price = 0, int length = 0) : m_price(price), m_length(length) {
		cout << "Car::Car(int price = 0, int length = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price), m_length(car.m_length) {
		cout << "Car::Car(const Car& car)" << endl;
	}
	void display() {
		cout << "price = " << m_price << ", length = " << m_length << endl;;
	}
};
int main() {
	Car car1(100, 200);
	car1.display();
	Car car2(car1);
	car2.display();
	getchar();
	return 0;
}

但是当我们没有拷贝构造函数的时候 那么他可以完成对象的创建吗 如果可以的话 那么是如何进行拷贝操作的呢

class Car {
	int m_price;
	int m_length;
public:
	Car(int price = 0, int length = 0) : m_price(price), m_length(length) {
		cout << "Car::Car(int price = 0, int length = 0)" << endl;
	}
	void display() {
		cout << "price = " << m_price << ", length = " << m_length << endl;;
	}
};
int main() {
	Car car1(100, 200);
	car1.display();
	Car car2(car1);
	car2.display();
	getchar();
	return 0;
}

上述案例中 是可以根据旧对象完成新对象的创建的 至于如何拷贝 我们得通过汇编去窥探一下细节
在这里插入图片描述
可以看到 就算没有使用拷贝构造函数去储存拷贝操作的话 那么系统也会内置默认的拷贝操作 可以看到 拷贝的源头地址值是连续的 可以说明这是拷贝的源头对象的两个成员变量 拷贝的目标地址值也是连续的 说明这是拷贝的目标对象的两个成员变量
所以上述的汇编就等价于

Car car2(car1);
car2.m_price = car1.m_price;
car2.m_length = car1.m_length;

而且在这个汇编中 值得注意的是他是分了两次进行拷贝的 为什么不一次性拷贝完呢 这是因为我运行的环境是x86环境 寄存器最大也就占用4个字节 所以挪动到寄存器中的数据最多也就是4个字节 但是如果采用的是x64的运行环境 那么就可以一次性拷贝8个字节了

那么既然系统已经内置了默认的拷贝操作的话 那为什么还需要通过拷贝构造函数的方式来完成拷贝操作呢 这是因为有时候我们可能需要一些个性化的操作 例如我们可能只想要完成其中一部分成员变量的拷贝操作 并不像要像内置的拷贝操作那样完成所有成员变量的拷贝操作 这种情景下利用拷贝构造函数进行拷贝操作才是最合适的

1.调用父类的拷贝构造函数

子类构造函数中调用父类的构造函数有一些规则:
1.如果父类没有构造函数的话 那么子类就不会去调用父类的构造函数
2.子类会自动调用父类的无参构造函数
3.如果父类中没有无参构造函数的话 那么子类就需要去显式调用有参构造函数
4.子类显式调用有参构造函数的话 就不会再去自动调用无参构造函数了

我们先来说一下系统的默认行为 也就是在没有拷贝构造函数下的拷贝操作是怎样的

class Person {
	int m_age;
public:
	Person(int age) : m_age(age) {

	}
	int getAge() {
		return m_age;
	}
};
class Student : public Person {
	int m_score;
public:
	Student(int age, int score) : Person(age), m_score(score) {

	}
	void display() {
		cout << "age = " << getAge() << ", score = " << m_score << endl;
	}
};
int main() {
	Student stu1(18, 90);
	stu1.display();
	Student stu2(stu1);
	stu2.display();
	getchar();
	return 0;
}

我们可以看到 默认行为其实就是将所有的成员变量统统拷贝干净即可

再来看一下如何通过拷贝构造函数完成所有成员的拷贝操作

class Person {
	int m_age;
public:
	Person(int age) : m_age(age) {

	}
	Person(const Person& person) : m_age(person.m_age){
	
	}
	int getAge() {
		return m_age;
	}
};
class Student : public Person {
	int m_score;
public:
	Student(int age, int score) : Person(age), m_score(score) {

	}
	Student(const Student& stu) : Person(stu), m_score(stu.m_score){

	}
	void display() {
		cout << "age = " << getAge() << ", score = " << m_score << endl;
	}
};
int main() {
	Student stu1(18, 90);
	stu1.display();
	Student stu2(stu1);
	stu2.display();
	getchar();
	return 0;
}

可能会有这么一个疑问 就是为什么调用父类的拷贝构造函数可以完成对子类中父类成员变量的赋值操作呢 不应该是父类中父类成员变量的赋值操作吗
这是因为this指针的存在 我们可以理解Person(stu)为this.Person(stu) 这个this为Student对象 即子类对象 进入函数内部 m_age也可以理解为this.m_age 即Student对象中的m_age成员

2.注意点

class Car {
	int m_price;
	int m_length;
public:
	Car(int price = 0, int length = 0) : m_price(price), m_length(length){
		cout << "Car::Car(int price = 0, int length = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price), m_length(car.m_length){
		cout << "Car::Car(const Car& car)" << endl;
	}
	void display() {
		cout << "price =  " << m_price << ", length = " << m_length << endl;
	}
};
int main() {
	Car car1(100, 5);// Car::Car(int price = 0, int length = 0)
	Car car2(car1);// Car::Car(const Car& car)
	Car car3 = car2;// Car::Car(const Car& car)
	Car car4;// Car::Car(int price = 0, int length = 0)
	car4 = car3;
	car4.display();
	getchar();
	return 0;
}

上述案例中 一共创建了4个对象 其中Car car3 = car2;这种写法等价于Car car3(car2); 即利用旧的对象创建新的对象
有的人可能会奇怪 为什么car4 = car3;这个语句没有调用拷贝构造函数呢 这是因为构造函数是在创建对象的同时进行的 这个语句压根没有涉及对象的创建 更别说调用构造函数了 但是这个语句的的确确也是执行了拷贝操作 只不过执行的是系统默认的拷贝操作 即对所有的成员变量进行拷贝

对于下面这个案例 可能有人和疑惑说为什么我明明没有调用父类的拷贝构造函数完成子类中父类成员的赋值操作 为什么父类成员的值是0(可能调用了父类的无参构造函数所致) 而不是0xcccc(栈空间会用0xcccc填充消除垃圾数据)呢
确实调用了父类的无参构造函数 即Person(int age = 0)(提供了默认参数 所以可以看成无参构造函数) 本质上就是对子类中父类成员赋值为了默认参数 即0

class Person {
	int m_age;
public:
	Person(int age = 0) : m_age(age) {

	}
	Person(const Person& person) : m_age(person.m_age) {

	}
	int getAge() {
		return m_age;
	}
};
class Student : public Person {
	int m_score;
public:
	Student(int age = 0, int score = 0) : Person(age), m_score(score) {

	}
	Student(const Student& stu) : m_score(stu.m_score) {

	}
	void display() {
		cout << "age = " << getAge() << ", score = " << m_score << endl;
	}
};
int main() {
	Student stu1(18, 100);
	Student stu2(stu1);
	stu2.display();// age = 0, score = 100
	getchar();
	return 0;
}

3.浅拷贝、深拷贝

在此之前 我们先来回顾一下C语言/C++中字符串的相关知识

int main() {
	// 通过字符数组储存字符串 他的本质是储存了字符串的内容 并非是指向了这个字符串 所以说是允许修改的
	char str1[] = "bmw";
	// 通过字符指针指向字符串 他的本质则是指向了字符串字面量 字面量不可修改 所以需要在*之前加上const用于限制内容的可修改性
	const char* str2 = "bmw";
	getchar();
	return 0;
}

还有就是数组名的本质是一个地址值 是数组中首元素的地址值 他的类型就是指向首元素指针的类型

编译器默认提供的拷贝就是浅拷贝

class Car {
	int m_price;
	char* m_name;
public:
	Car(int price = 0, char* name = NULL) : m_price(price), m_name(name) {

	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
int main() {
	char name[] = { 'b', 'm', 'w', '\0' };
	Car* car = new Car(100, name);
	getchar();
	return 0;
}

在这里插入图片描述
上述这个案例中 堆空间指向了栈空间 这种行为是非常危险的 我们可以举以下这个例子来说明他的危险性

class Car {
	int m_price;
	char* m_name;
public:
	Car(int price = 0, char* name = NULL) : m_price(price), m_name(name) {

	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
Car* g_car;
void test() {
	char name[] = "bmw";
	g_car = new Car(100, name);
}
int main() {
	test();
	g_car->display();
	getchar();
	return 0;
}

当test函数调用完毕以后 name所在的栈空间就要被回收 但是回收不代表g_car的值发生改变 反之是没有改变的 但是回收说明了这段内存可能被别人使用 所以说我们现在利用g_car访问的那段内存很有可能是别人的内存 而不是预期的原来数据 当我们调用display()函数时 打印m_name 这是一个字符串 需要碰到第一个’\0’才停止截取 如果是原来的数据那肯定是碰得到的 但是现在访问的很有可能是别人的数据 那么你就不知道什么时候才可以碰到第一个’\0’了 所以堆空间指向栈空间这种行为是很危险的 很容易造成野指针 即指针指向了一段已经被回收的内存

但是上述这个案例如何去规避这个风险呢
解决方案就是在堆空间中分配一段指定数组大小的内存 然后将栈空间数组的数据拷贝到堆空间中 那样也就不会产生野指针了

class Car {
	int m_price;
	char* m_name;
public:
	Car(int price = 0, char* name = NULL) : m_price(price){
		if (name == NULL)return;
		// 下面语句中的{}的作用在于提前为字符串设置好结束标记'\0'
		m_name = new char[strlen(name) + 1] {};
		strcpy(m_name, name);
	}
	~Car() {
		if (m_name == NULL)return;
		delete[] m_name;
		m_name = NULL;
	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
int main() {
	char name[] = { 'b', 'm', 'w', '\0' };
	Car* car = new Car(100, name);
	car->display();
	getchar();
	return 0;
}

我们可以看一下内存中的细节
在这里插入图片描述
我们可以发现 红框中的地址值是不一样的 说明堆空间中的对象不在指向栈空间的数组 而是指向了堆空间的数组 有效的规避了野指针的产生

而且就算外面的数组是申请在堆空间中的 那么也很有可能产生野指针
下面这个案例先行释放name数组 然后在调用display 打印m_name 此时m_name指向的数组已经被回收了 所以会访问到不符合预期的数据 并且产生野指针

class Car {
	int m_price;
	char* m_name;
public:
	Car(int price = 0, char* name = NULL) : m_price(price){
		if (name == NULL)return;
		m_name = new char[strlen(name) + 1] {};
		strcpy(m_name, name);
	}
	~Car() {
		if (m_name == NULL)return;
		delete m_name;
		m_name = NULL;
	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
int main() {
	char* name = new char[4] {'b', 'm', 'w', '\0'};
	Car* car = new Car(100, name);
	delete[] name;
	car->display();
	getchar();
	return 0;
}

所以不管你申请的数组对象是在栈空间、堆空间还是全局区 我的做法统一都是在堆空间中申请一段内存用于存放外面这个数组对象的数据

以下这个案例中 两个对象的m_name指向的是同一个数组 这样显然不合理 因为其中一个对象对于m_name的改变会影响到另外一个对象中的m_name 我们都知道 每一个对象中的属性是相互独立的 不应该是相互影响的关系 再者到时候释放内存时 由于存在两个对象 导致会产生double free的现象 也就是对同一个对象内存释放了两次 实际上释放一次即可

class Car {
	int m_price;
	char* m_name;
public:
	Car(int price = 0, const char* name = NULL) : m_price(price){
		if (name == NULL)return;
		m_name = new char[strlen(name) + 1] {};
		strcpy(m_name, name);
	}
	Car(const Car& car) : m_name(car.m_name), m_price(car.m_price){}
	~Car() {
		if (m_name == NULL)return;
		delete m_name;
		m_name = NULL;
	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
int main() {
	Car car1(100, "bmw");
	Car car2(car1);
	car2.display();
	getchar();
	return 0;
}

那如何解决上述的这个问题 我们可以让这两个对象分别指向不同的name数组 也就是说 在堆空间中在申请一段内存 然后将name数组中的内容拷贝到这段新申请的内存

class Car {
	int m_price;
	char* m_name;
public:
	Car(int price = 0, const char* name = NULL) : m_price(price){
		if (name == NULL)return;
		m_name = new char[strlen(name) + 1] {};
		strcpy(m_name, name);
	}
	Car(const Car& car) : m_price(car.m_price){
		if (car.m_name == NULL)return;
		m_name = new char[strlen(car.m_name) + 1] {};
		strcpy(m_name, car.m_name);
	}
	~Car() {
		if (m_name == NULL)return;
		delete m_name;
		m_name = NULL;
	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
int main() {
	Car car1(100, "bmw");
	Car car2(car1);
	car2.display();
	getchar();
	return 0;
}

按照上述的代码就可以解决之前的那两个弊端了
我们也可以观察一下内存细节 看一下这两个对象指向的name数组是否不同
在这里插入图片描述
可以发现 果真如此
但是上述代码 还有值得优化的地方 就是抽取公共代码 提高代码的复用率 即抽取深拷贝的逻辑
在此之前 我们要先来介绍一下何为浅拷贝 何为深拷贝
浅拷贝:对于指针类型的变量 只会拷贝他所储存的地址值 正如该案例的第一种做法那样
深拷贝:将指针指向的内容拷贝到新的内存空间中
两者最显著的区别就在于有无创建新的内存空间

class Car {
	int m_price;
	char* m_name;
	void copy(const char* name) {
		if (name == NULL)return;
		m_name = new char[strlen(name) + 1] {};
		strcpy(m_name, name);
	}
public:
	Car(int price = 0, const char* name = NULL) : m_price(price){
		copy(name);
	}
	Car(const Car& car) : m_price(car.m_price){
		copy(car.m_name);
	}
	~Car() {
		if (m_name == NULL)return;
		delete m_name;
		m_name = NULL;
	}
	void display() {
		cout << "price is " << m_price << ", name is " << m_name;
	}
};
int main() {
	Car car1(100, "bmw");
	Car car2(car1);
	car2.display();
	getchar();
	return 0;
}

深浅拷贝是针对指针而言的 浅拷贝是地址拷贝 而深拷贝则是内容拷贝 往大了说 浅拷贝就是拷贝指针变量储存的地址值 而深拷贝则是申请一块新的内存空间用于存放指针所指向的内容
但是 具体要不要深拷贝 取决于你的需求 像上面的两块栈空间共用一块堆空间的案例 如果你的需求就是想要共用的话 那么不需要深拷贝 直接使用默认的浅拷贝即可 但是如果说你想要这两块栈空间分别指向两块独立的堆内存的话 那么你就需要自定义拷贝构造函数进行深拷贝了

class Car {
	int m_price;
public:
	Car(int price) : m_price(price){}
	int getPrice() {
		return m_price;
	}
};
class Person {
	int m_age;
	Car m_car;
public:
	Person(int age, int price) : m_age(age), m_car(price) {
		
	}
	void display() {
		cout << "age = " << m_age << ", price = " << m_car.getPrice() << endl;
	}
};
int main() {
	Person person1(20, 100);
	person1.display();
	Person person2 = person1;
	person2.display();
	getchar();
	return 0;
}

上述案例就等价于

class Person {
	int m_age;
	int m_price;
public:
	Person(int age, int price) : m_age(age), m_car(price) {
		
	}
	void display() {
		cout << "age = " << m_age << ", price = " << m_car.getPrice() << endl;
	}
};
int main() {
	Person person1(20, 100);
	person1.display();
	Person person2 = person1;
	person2.display();
	getchar();
	return 0;
}

这是因为m_car是位于Person对象栈内存中的一部分栈内存 所以相当于将m_car中的成员变量拿到Person对象的栈内存中
你看 这个案例他就属于浅拷贝

class Car {
	int m_price;
public:
	Car(int price) : m_price(price){}
	int getPrice() {
		return m_price;
	}
};
class Person {
	int m_age;
	Car* m_car;
public:
	Person(int age, Car* car) : m_age(age), m_car(car){
		
	}
	void display() {
		cout << "age is " << m_age << ", price is " << m_car->getPrice() << endl;
	}
};
int main() {
	Car car1(100);
	Person person1(20, &car1);
	person1.display();
	Person person2 = person1;
	person2.display();
	getchar();
	return 0;
}

上述这个案例中 我们让两个对象的指针指向了同一个堆内存 从而导致了double free现象的出现 但是如果你的需求是两个对象共用一份堆内存的话 那么这个浅拷贝的做法是正确的
但是如果你是想要让两个对象的指向不同的话 那么就需要采用深拷贝的做法 通过拷贝构造函数内置深拷贝的做法

class Car {
	int m_price;
public:
	Car(int price) : m_price(price){}
	int getPrice() {
		return m_price;
	}
	void setPrice(int price) {
		m_price = price;
	}
};
class Person {
	int m_age;
	Car* m_car;
public:
	Person(int age, Car* car) : m_age(age), m_car(car){
		
	}
	Person(const Person& person) : m_age(person.m_age) {
		if (person.m_car == NULL)return;
		// 分配一段长度一样的堆内存
		m_car = (Car*)malloc(sizeof(person.m_car));
		// 然后将旧内存中的数据拷贝到新内存中
		m_car->setPrice(((person.m_car)->getPrice()));
	}
	~Person() {
		if (m_car == NULL)return;
		m_car = NULL;
		free(m_car);
	}
	void display() {
		cout << "age is " << m_age << ", price is " << m_car->getPrice() << endl;
	}
};
int main() {
	Car car1(100);
	Person person1(20, &car1);
	person1.display();
	Person person2 = person1;
	person2.display();
	getchar();
	return 0;
}

27.对象类型的参数和返回值

当对象类型作为参数或者返回值的时候 就有可能产生不必要的中间对象

当对象类型作为参数的时候 以下是关于这个命题的案例

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
};
void test(Car car) {
	
}
int main() {
	Car car1;// Car::Car(int price = 0)
	test(car1);// Car::Car(const Car& car)
	getchar();
	return 0;
}

我们可以将参数的传递看成一个赋值语句 即Car car = car1; 这个语句相当于在创建对象的同时进行了浅拷贝操作 由于创建了一个新对象 并且进行了拷贝操作 所以需要调用拷贝构造函数
中间对象的产生的必要性是取决于你的具体需求的 如果你是想要在函数内部访问外面传递进来的这个对象 那么显然中间对象的产生是没有必要的 并且将对象参数替换成指针或者引用指向外面这个对象显然才是可行的

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
	// 提供一个getter方法 用于访问私有的成员变量
	int getPrice() {
		return m_price;
	}
};
void test(Car* car) {
	cout << car->getPrice() << endl;// 0
}
int main() {
	Car car1;// Car::Car(int price = 0)
	test(&car1);// Car::Car(const Car& car)
	getchar();
	return 0;
}

当对象类型作为返回值的时候 以下是关于这个命题的案例(一次拷贝构造 两次普通构造)

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
	// 提供一个getter方法 用于访问私有的成员变量
	int getPrice() {
		return m_price;
	}
};
Car test() {
	Car car2;// Car::Car(int price = 0)
	return car2;
}
int main() {
	Car car1;// Car::Car(int price = 0)
	car1 = test();
	getchar();
	return 0;
}

可能大家会有这么一个疑问 test函数一旦执行完毕以后 那么car2的内存将要被回收 那么到时候可能由car2拷贝给car1的数据就是错误的 因为一旦回收以后 他的内存和有可能被其他人占据 里面的数据也很有可能是其他人的 所以编译器会有以下行为来解决这个问题:
他会在main函数中取出一个未被占据的地址值 传递进test函数中 然后根据这个地址值创建一个全新的对象 并由car2对其进行拷贝操作 然后在main函数中再有这个全新对象对car1进行拷贝操作 所以说 你可以看到结果会有一个拷贝构造函数的调用 而且显然这个中间对象的产生是十分之有必要的

我们在来看一下另外一个案例(一次拷贝构造 一次普通构造)

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
	// 提供一个getter方法 用于访问私有的成员变量
	int getPrice() {
		return m_price;
	}
};
Car test() {
	Car car2;
	return car2;
}
int main() {
	Car car1 = test();// Car::Car(int price = 0)
	getchar();
	return 0;
}

该案例和上一个案例很相似 都是将main占内存中的一个未被分配的地址值传递入test函数中 然后在内部根据这个地址值创建一个全新的对象 然后用car2对其进行拷贝操作
但是很凑巧的是 刚好要分配给car1的地址值就是一个未被分配的地址值 索性将该地址值直接传入test函数中(因为=的执行顺序是从右往左 所以先执行test函数 在分配内存给car1) 然后由car2对这个全新的对象进行拷贝操作

但是大家有可能因为版本的问题 看不到拷贝构造函数的打印结果 这是因为编译器进行了返回值优化

1.匿名对象(临时对象)

所谓匿名对象 就是使用之后立马销毁的对象

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
	~Car() {
		cout << "Car::~Car()" << endl;
	}
	void run() {
		cout << "Car::run()" << endl;
	}
	// 提供一个getter方法 用于访问私有的成员变量
	int getPrice() {
		return m_price;
	}
};
int main() {
	cout << 1 << endl;// 1
	Car().run();// Car::Car(int price = 0) Car::run() Car::~Car()
	cout << 2 << endl;// 2
	getchar();
	return 0;
}

当对象类型作为参数的时候 接受的是一个匿名对象 那么他所执行的操作就是直接将该匿名对象作为形参对象

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
	~Car() {
		cout << "Car::~Car()" << endl;
	}
	void run() {
		cout << "Car::run()" << endl;
	}
	// 提供一个getter方法 用于访问私有的成员变量
	int getPrice() {
		return m_price;
	}
};
void test(Car car) {

}
int main() {
	test(Car());
	getchar();
	return 0;
}

当对象类型作为返回值的时候 返回的是一个匿名对象 那么他会直接将返回值中的匿名对象拿到test()位置处

class Car {
	int m_price;
public:
	Car(int price = 0) : m_price(price) {
		cout << "Car::Car(int price = 0)" << endl;
	}
	Car(const Car& car) : m_price(car.m_price){
		cout << "Car::Car(const Car& car)" << endl;
	}
	~Car() {
		cout << "Car::~Car()" << endl;
	}
	void run() {
		cout << "Car::run()" << endl;
	}
	// 提供一个getter方法 用于访问私有的成员变量
	int getPrice() {
		return m_price;
	}
};
Car test() {
	return Car();
}
int main() {
	Car car2;
	car2 = test();
	getchar();
	return 0;
}

28.隐式构造

以下案例中 Person p1 = 20;这种写法相当于调用了Person的单参构造函数 该参数为20

class Person {
	int m_age;
public:
	Person() {
		cout << "Person::Person()" << this << endl;
	}
	Person(int age) : m_age(age) {
		cout << "Person::Person(int age)" << this << endl;
	}
	Person(const Person& person) {
		cout << "Person::Person(const Person& person)" << this << endl;
	}
	~Person() {
		cout << "Person::~Person()" << this << endl;
	}
	void display() {
		cout << "Person::display() - age is" << m_age << endl;
	}
};
int main() {
	// 以下两种写法是等价的
	Person p1 = 20;// Person::Person(int age)00B7F998
	Person p1(20);// Person::Person(int age)00BBFD6C
	getchar();
	return 0;
}

当然上述结论也可以运用于对象类型作为参数的情景 以下案例中 会通过Person person(3)创建一个Person对象

class Person {
	int m_age;
public:
	Person() {
		cout << "Person::Person()" << this << endl;
	}
	Person(int age) : m_age(age) {
		cout << "Person::Person(int age)" << this << endl;
	}
	Person(const Person& person) {
		cout << "Person::Person(const Person& person)" << this << endl;
	}
	~Person() {
		cout << "Person::~Person()" << this << endl;
	}
	void display() {
		cout << "Person::display() - age is" << m_age << endl;
	}
};
void test(Person person) {

}
int main() {
	test(3);
	getchar();
	return 0;
}

既然可以应用于对象类型作为参数的情景 自然也可以应用于对象类型作为返回值的情景 其中会通过Person person(40)创建一个全新的Person对象

class Person {
	int m_age;
public:
	Person() {
		cout << "Person::Person()" << this << endl;
	}
	Person(int age) : m_age(age) {
		cout << "Person::Person(int age)" << this << endl;
	}
	Person(const Person& person) {
		cout << "Person::Person(const Person& person)" << this << endl;
	}
	~Person() {
		cout << "Person::~Person()" << this << endl;
	}
	void display() {
		cout << "Person::display() - age is" << m_age << endl;
	}
};
Person test() {
	return 40;
}
int main() {
	test();
	getchar();
	return 0;
}

接下来这个案例中 Person p1;p1 = 40;这种写法可不等价于Person p1 = 40;
前者是类型不匹配 导致40转换为Person匿名对象 由匿名对象对p1进行拷贝操作
后者则是根据40创建一个全新的Person对象

class Person {
	int m_age;
public:
	Person() {
		cout << "Person::Person()" << this << endl;
	}
	Person(int age) : m_age(age) {
		cout << "Person::Person(int age)" << this << endl;
	}
	Person(const Person& person) {
		cout << "Person::Person(const Person& person)" << this << endl;
	}
	~Person() {
		cout << "Person::~Person()" << this << endl;
	}
	void display() {
		cout << "Person::display() - age is" << m_age << endl;
	}
};
int main() {
	Person p1;
	p1 = 40;
	getchar();
	return 0;
}

以上的这些案例 无不体现着C++中存在隐式构造的现象(某些情况下 会隐式调用单参构造函数或者一个默认参数的双参构造函数)
我们可以用explicit关键字(明确的意思 明着调用)来禁止隐式构造 具体的行为就是用explicit修饰单参构造函数

class Person {
	int m_age;
public:
	Person() {
		cout << "Person::Person()" << this << endl;
	}
	explicit Person(int age) : m_age(age) {
		cout << "Person::Person(int age)" << this << endl;
	}
	Person(const Person& person) {
		cout << "Person::Person(const Person& person)" << this << endl;
	}
	~Person() {
		cout << "Person::~Person()" << this << endl;
	}
	void display() {
		cout << "Person::display() - age is" << m_age << endl;
	}
};
int main() {
	Person p1;
	p1 = 40;// error
	getchar();
	return 0;
}

29.编译器生成的构造函数

在某些情况下(具体就是构造函数有事情干的时候) 编译器会自动为类生成无参构造函数 比如:
1.成员变量在声明的同时进行了初始化操作
构造函数需要完成成员变量的初始化操作

class Person {
	int m_age = 10;
};
int main() {
	Person p;
	getchar();
	return 0;
}

在这里插入图片描述
2.定义了虚函数
构造函数需要完成对象前面4个字节分配给虚表地址的操作

class Person {
	int m_age;
public:
	virtual void run() {
		cout << "Person::run()" << endl;
	}
};
int main() {
	Person p;
	getchar();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
3.虚继承了其他类
构造函数需要完成对象前面4个字节分配给虚表地址的操作

class Person {

};
class Student : virtual Person {

};
int main() {
	Student student;
	getchar();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
4.包含了对象类型的成员 并且这个成员有构造函数(编译器生成或者自定义)
构造函数需要完成对象成员创建时构造函数的调用

class Car {
	int m_price;
public:
	Car(){}
};
class Person {
	Car car;
};
int main() {
	Person person;
	getchar();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
5.父类有构造函数(编译器生成或者自定义)
构造函数需要完成父类构造函数的调用操作

class Person {
public:
	Person(){}
};
class Student : public Person {

};
int main() {
	Student student;
	getchar();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
总之 如果对象创建后 需要做一些额外的操作(内存操作、函数调用) 那么编译器就会为其生成构造函数
请注意 对象创建后这个时机指的不是源文件中对象创建语句之后 而是发生在源文件中对象创建语句对应汇编代码中
以下这个案例编译器是不会生成构造函数的

class Student {
public:
	void run() {
		cout << "Student::run()" << endl;
	}
};
int main() {
	Student student;
	student.run();
	getchar();
	return 0;
}

30.友元

友元包括友元函数和友元类
如果将函数A(非成员函数 即类中的成员函数不能声明为友元函数)声明为类C的友元函数 那么在函数A中就可以直接访问类C对象的所有成员
同样 如果将类A声明为类C的友元类 那么在类A的所有成员函数中就可以直接访问类C对象的所有成员

以下案例中 add方法每调用一次 都需要额外调用四次的getter函数 必然需要额外开辟四份栈空间 万一add方法调用频繁的话 那么栈空间的开支就十分庞大了 所以有没有一种方法可以让add方法内部直接访问Point的私有成员 又不想要让其他函数直接访问Point的私有成员

class Point {
	int m_x;
	int m_y;
public:
	int getX() {
		return m_x;
	}
	int getY() {
		return m_y;
	}
	Point(int x, int y) : m_x(x), m_y(y){}
	void display() {
		cout << "(" << m_x << ", " << m_y << ")" << endl;
	}
};
void test() {
	Point p1(10, 20);
	p1.m_x = 30;// error
}
Point add(Point p1, Point p2) {
	return Point(p1.getX() + p2.getX(), p1.getY() + p2.getY());
}
int main() {
	Point p1(10, 20);
	Point p2(20, 30);
	Point p3 = add(p1, p2);
	p3.display();
	getchar();
	return 0;
}

前面这个问题 显然是有答案的 那就是将add函数声明为Point的友元函数 一旦该函数被声明为了Point的友元函数 那么就可以访问Point类中的所有成员了

class Point {
	// 该声明可以放置在Point类中的任何地方 包括private权限范围内
	friend Point add(Point, Point);
	int m_x;
	int m_y;
public:
	int getX() {
		return m_x;
	}
	int getY() {
		return m_y;
	}
	Point(int x, int y) : m_x(x), m_y(y){}
	void display() {
		cout << "(" << m_x << ", " << m_y << ")" << endl;
	}
};
void test() {
	Point p1(10, 20);
	p1.m_x = 30;// error
}
Point add(Point p1, Point p2) {
	return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}
int main() {
	Point p1(10, 20);
	Point p2(20, 30);
	Point p3 = add(p1, p2);
	p3.display();
	getchar();
	return 0;
}

以下案例中 将Math类声明为了Point类的朋友 让Math类中的所有成员函数可以直接访问Point类对象中的所有成员

class Point {
	friend class Math;
	int m_x;
	int m_y;
public:
	int getX() {
		return m_x;
	}
	int getY() {
		return m_y;
	}
	Point(int x, int y) : m_x(x), m_y(y){
		cout << "Point::Point(int x, int y)" << endl;
	}
	~Point() {
		cout << "Point::~Point()" << endl;
	}
	void display() {
		cout << "(" << m_x << ", " << m_y << ")" << endl;
	}
};
class Math {
public:
	void add() {
		cout << Point(10, 20).m_x << endl;
	}
};
int main() {
	Math().add();
	getchar();
	return 0;
}

31.内部类

如果将类A定义在类C的内部 那么类A就是一个内部类(嵌套类)
内部类的特点:
支持public、protected、private权限(可以通过这些权限来修改内部类的访问范围)
成员函数可以直接访问其外部类对象的所有成员(反过来则不行 就是外部类的成员函数不可以直接访问其内部类的所有成员)

class Person {
	int m_age;
public:
	Person() {}
	class Car {
		int m_price;
	public:
		Car(){}
		void run() {
			Person person;
			person.m_age = 10;
			cout << person.m_age << endl;
		}
	};
};
int main() {
	Person::Car car;
	car.run();
	getchar();
	return 0;
}

内部类的成员函数可以直接不带类名访问外部类的所有static成员

class Person {
	static int m_age;
	static void eat(){
		cout << "Person::eat()" << endl;
	}
public:
	Person() {}
	class Car {
		int m_price;
	public:
		Car(){}
		void run() {
			m_age = 20;
			eat();
		}
	};
};
int Person::m_age = 0;
int main() {
	Person::Car car;
	car.run();
	getchar();
	return 0;
}

不会影响外部类的内存布局(其实布局方式就同内部类写在外部类外面一样 只不过改变的是内部类的访问权限而已) 其内存布局与内部类和外部类平级时相同
内部类声明和实现分离的方式:
第一种方式就是将内部类中的函数声明和实现进行分离

class Person {
	int m_age;
public:
	Person() {}
	class Car {
	public:
		void run();
	};
};
void Person::Car::run() {
	cout << "Person::Car::run()" << endl;
}
int main() {
	Person::Car car;
	car.run();
	getchar();
	return 0;
}

第二种方式就是将内部类的声明和实现分离

class Person {
	int m_age;
public:
	Person() {}
	class Car;
};
class Person::Car {
public:
	Car() {};
	void run() {
		cout << "Person::Car::run()" << endl;
	}
};
int main() {
	Person::Car car;
	car.run();
	getchar();
	return 0;
}

第三种方式基于第二种方式 将外面的类的定义中将函数的声明和定义分离

class Person {
	int m_age;
public:
	Person() {}
	class Car;
};
class Person::Car {
public:
	Car() {};
	void run();
};
void Person::Car::run() {
	cout << "Person::Car::run()" << endl;
}
int main() {
	Person::Car car;
	car.run();
	getchar();
	return 0;
}

32.局部类

在一个函数内部定义的类 称为局部类
局部类的特点:
作用域仅限于所在函数内部
所有成员的定义必须放在类内部(不允许成员的声明和定义分离) 也不允许定义static成员变量(static成员变量需要在类外面进行初始化)
成员函数是不可以直接访问函数的局部变量(static变量除外 因为非static局部变量是需要调用函数分配栈空间而来的 而访问函数的局部变量显然不用调用函数 static则不然 其不需要依附于函数栈空间存在)
局部类和内部类的区别在于他们的访问范围的不同 局部类依然不会改变其外部函数的内存布局 局部类的内存布局与类在函数外面时相同

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axihaihai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值