构造函数的概念与特点
C++构造函数和Java的构造函数是类似的。当创建一个对象时,构造函数会被自动调用。可以在创建对象时初始化对象的成员变量。构造函数具有如下特点:
- 构造函数的名称与类名称相同
- 没有返回值
- 可以有参数,也可以没参数
- 如果没有定义构造函数,则编译器会提供一个默认的无参构造函数
- 构造函数可以重载
在下面的demo中申明了一个无参构造,一个有参构造函数。注意:这里有个最佳实践的技巧,c++不需要每个成员都声明其访问修饰符,类成员的访问修饰符( public
、 private
、 protected
)通常是写在一起的,并且修饰符只需要在第一个成员前面进行声明。这样可以提高代码的可读性和简洁性。这里只是建议大家不要在c++中写出java风格的代码。
class Student {
public:
Student() {} //无参构造函数
Student(string name, int age); //有参构造函数
private:
string _name; //c++中私有成员变量建议_开头,这是一种规范,提高代码可读性
int _age;
};
注意,上面只是类的申明,下面才是类的定义。在实际的c++项目开发中,类的申明和定义一般是分开的,类的申明是在头文件.h
中,定义(也就是具体的实现)在源文件.cpp
中。 这里先有个概念即可,后面会有一节专门来讲这个。
初始化成员变量
//类的有参构造函数的定义
Student::Student(string name, int age) {
this->_name = name;
this->_age = age;
}
这里是初始化了2个成员变量。我们在Java中很多时候就是用的这种方式来初始化成员变量,但是在C++中不建议这种方式,而是提供了一种初始化成员列表
的方式。
//初始化成员列表的方式
Student::Student(string name, int age): _name(name),_age(age) {
cout<<"name: "<<_name<<endl;
cout<<"age: "<<_age<<endl;
}
有以下几种情况是要求必须在初始化成员列表中初始化的
- const成员变量
- 引用类型成员变量
- 类对象
创建对象的几种方式
int main() {
//推荐方式
Studen stu;
Studen stu2("zx", 18);
Studen stu3 = Studen(); //不建议
//new是运算符,不是关键字,用于堆内存创建对象,注意,需要手动释放指针
Studen *stu4 = new Studen("lisi",20);
return 0;
}
在C++中, new
是一个运算符,用于在堆上动态分配内存来创建对象。它返回一个指向新分配对象的指针。常规的对象创建不建议用这种方式,因为使用复杂,还需要手动释放指针。
析构函数概念与使用
C++中的析构函数是一种特殊的成员函数,用于在对象被销毁时执行清理操作。
析构函数具有如下特点:
- 析构函数的名称与类名相同,但前面加上一个波浪号
~
作为前缀。 - 析构函数没有返回值,也不接受任何参数。
- 自动调用
- 如果类中没有显式定义析构函数,编译器会自动生成一个默认的析构函数。
析构函数在以下情况下被自动调用:
- 当对象的生命周期结束时,即对象超出其作用域。
- 当通过delete运算符显式销毁动态分配的对象。
- 当对象是局部对象并且位于函数的栈帧中,在函数执行完毕后会自动调用析构函数。
class Studen {
public:
Studen(string name, int age) : _name(name), _age(age) {
//堆内存申请空间,指针指向该地址
arr = new char[10];
};
~Studen() {
//释放指针
delete[] arr;
}
private:
string _name;
int _age;
char *arr;
};
析构函数的主要作用是释放对象所占用的资源,例如释放动态分配的内存、关闭文件、释放锁等。需要注意的是,如果类中有指针成员变量或资源需要手动释放,就需要显式定义析构函数来执行相应的清理操作。在析构函数中,可以使用delete运算符释放动态分配的内存,或者调用其他适当的清理函数来释放资源。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数。接下来探讨这几个问题
- 拷贝构造函数是什么?怎么用?
- 拷贝构造函数触发时机?使用场景?
- 浅拷贝与深拷贝的区别?
- 深拷贝如何实现?
- 一些最佳实践的经验
下面的Demo定义了User类,类中定义了无参构造函数,有参构造函数,析构函数,以及2个成员变量。
class User {
public:
User() {
cout << "User()" << endl;
};
User(long uid, int sex) : uid(uid), sex(sex) {
cout << " User(long uid,int sex)" << endl;
}
~User() {
cout << " ~User()" << endl;
}
long uid;
int sex;
};
main
方法
int main() {
User user1(100L, 1);
User user2 = user1;
cout << "user2 uid: " << user2.uid << endl;
cout << "user2 sex: " << user2.sex << endl;
cout << &user1 << endl;
cout << &user2 << endl;
return 0;
}
}
这里先初始化了一个对象user1,然后通过创建好的user1去初始化一个对象user2。然后就是一些打印输出,我们先来看下结果:
输出如下
User(long uid,int sex)
user2 uid: 100
user2 sex: 1
0x7ff7bc5d77f8
0x7ff7bc5d77e0
~User()
~User()
这里创建user2对象的时候,看上去和java创建方式是不是很像? 而且从结果上看user2的变量的值和user1是一样的。但是我们通过&
取址运算符,发现2个对象指向的内存地址不一样。 在java中,User user2 = user1
,user2是个引用,和user1指向的确实是同一个内存地址,所以操作的是同一个对象。但是c++中,这种方式是创建了一个新的对象,只是这个新对象的成员变量的值和已经初始化对象的值一样。
这是啥原因呢?这其实是通过User user2 = user1
方式创建新对象的时候,c++编译器会为我们自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数执行的是浅拷贝,即仅仅将对象的成员变量逐个地复制到新对象中。
下面一起来看下拷贝构造函数是怎样的?
class User {
public:
User() {
cout << "User()" << endl;
};
User(long uid, int sex) : uid(uid), sex(sex) {
cout << " User(long uid,int sex)" << endl;
}
//拷贝构造函数
User(const User &user) {
cout << " User(const User& user)" << endl;
cout << " User(const User& user) user uid: " << user.uid << endl;
cout << " User(const User& user) user sex: " << user.sex << endl;
}
~User() {
cout << " ~User()" << endl;
}
long uid;
int sex;
};
当这样User user2 = user1
创建user2时,会自动调用拷贝构造函数。注意:只会调用拷贝构造,不会调用任何其他的构造函数。
User(const User& user)
User(const User& user) user uid: 100
User(const User& user) user sex: 1
这里是自己实现了拷贝构造函数,如果不写,编译器会自动生成一个拷贝构造函数,自动生成的代码大致如下:
//拷贝构造函数
User(const User &user) {
//自动为每一个成员变量赋值
uid = user.uid;
sex = user.sex;
}
拷贝构造函数的语法(必须这样)
- 与无参构造,有参构造是一样的,区别在于参数不同
- 参数只有一个,是当前类的引用
- const是为了避免方法内部通过引用去修改对象的属性
上面的调用,相当于user2 调用User(user1)把user1传给拷贝构造函数;由于user1和引用类型user是指向的同一个对象,如果不加const,则可能在拷贝构造函数内部修改user1的值,所以为了避免这种风险需要强制加const。
一个新手误区
如果我们自己实现了如下的拷贝构造函数,是有问题的。你会发现user引用是有值的,因为拷贝构造是被调用了,但是User user2 = user1
这里就不会把user1的变量(uid,sex)的值自动拷贝给user2了。所以说重写拷贝构造函数 的时候,需要手动赋值。
//拷贝构造函数
User(const User& user){
cout << " User(const User& user)" << endl;
cout << " User(const User& user) user uid: "<<user.uid << endl;
cout << " User(const User& user) user sex: "<<user.sex << endl;
}
浅拷贝与深拷贝
在上面例子User user2 = user1
已经说明编译器会自动创建并调用拷贝构造函数,就像调用默认的无参构造一样,而且不需要我们手动挨个给对象的成员变量赋值,这就是浅拷贝。
关于深拷贝,先不下定义,我们先把上面demo修改下,新增了一个变量address
,是个数组指针,然后在有参构造函数中通过new
运算符在堆内存上开辟了8 byte 的空间,在析构函数中释放指针。
class User {
public:
User() {
cout << "User()" << endl;
};
User(long uid, int sex) : uid(uid), sex(sex) {
cout << " User(long uid,int sex)" << endl;
address = new int[2];
address[0] = 100;
address[1] = 101;
}
~User() {
cout << " ~User()" << endl;
delete[] address;
}
void printAddress(){
for (int i = 0; i < 2; ++i) {
cout << address[i] << " ";
}
cout << endl;
}
long uid;
int sex;
int *address; //存储一堆地址的数组
};
main
方法
int main() {
User user1(100L, 1);
User user2 = user1;
cout << "user1 uid: " << user1.uid << endl;
cout << "user2 uid: " << user2.uid << endl;
cout << "user1 address: " << user1.address << endl;
cout << "user2 address: " << user2.address << endl;
cout << "user1 address value:";
user1.printAddress();
cout << "user2 address value:";
user2.printAddress();
return 0;
}
看下打印结果
可以发现user1和user2中 ,指针变量address
无论是内存地址还是数组中的值都是相同的。这是因为编译器自动生成的拷贝构造中,会自动为指针变量赋值,大致代码如下:
User(const User &user) {
this->address = user.address;
}
但是细心的你已经发现这里抛出了异常,而且main方法也不是return 0了(这是正常情况)。这个异常是说,要释放的指针还没有分配内存。
异常原因分析
因为
main
方法执行完之后,User对象就会销毁,就会执行析构函数,这里面会通过delete
运算符释放指针。从打印结果可以看到2个对象的析构函数都执行了,user1先释放了指针,user2再去释放指针时,由于2个对象的指针是指向的的同一个内存地址,所以相当于user2再去delete
的时候这个指针已经被释放了,这个操作是不被允许的,所以抛出了异常。
从上面的demo和分析可以看出,当类中有指针变量的时候,浅拷贝是存在问题的。这时候就需要深拷贝了,深拷贝的实现规则如下:
- 需要我们自己重写这个拷贝构造函数;
- 通过引用对成员变量赋值;
- 成员变量是指针,要指向一个新的地址(分配新的内存空间),不能通过引用去赋值;
- 指针变量指向的内存的值拷贝。
深拷贝关键代码如下:
User(const User &user) {
cout << " User(const User& user)" << endl;
uid = user.uid;
sex = user.sex;
//指向一个新的地址
address = new int[2];
//值的拷贝,sizeof是获取内存大小
std::memcpy(address, user.address, 2 * sizeof(int));
}
一些实践技巧
还是使用之前的User类,下面定义一个方法,方法的形参是User对象
void func(User user){ }
//main
int main() {
User user1(100L, 1);
func(user1);
}
//输出
User(long uid,int sex)
User(const User& user)
~User()
~User()
当函数形参是类对象的时候,也会调用拷贝构造函数,在方法执行完之后会调用析构函数。 因为本质上还是用一个已经创建的对象初始化一个新对象,所以这种方式会产生临时对象。可以通过引用来优化,代码如下:
void func(const User &user){ }
c++中建议使用引用作为函数的形参
总结
1、拷贝构造函数调用时机:当使用一个创建好的对象去初始化一个新的对象时,这个新对象的创建就会调用拷贝构造函数
2、一定注意拷贝构造函数的固定写法,只有一个connst 的引用作为参数;
3、默认是浅拷贝,浅拷贝会自动为所有变量赋值;如果类中有指针,就需要用深拷贝了,而深拷贝需要自己实现。