概念(作用):
- 用来在创建对象时,对对象进行初始化
特点
- 构造函数无返回值,函数名与类名相同,必须存在于public中
- 构造函数可以重载
- 不用来初始化static数据成员,因为static数据成员不属于类
默认构造函数(合成的构造函数)
- 如果没有在类中给出构造函数,编译器自动生成一个默认的构造函数(无参数、函数体为空)
- 当在类中手动给出构造函数时,默认的构造函数消失
- 编译器自动生成的默认的构造函数又称为“合成的默认构造函数”
- 默认构造函数做了什么:
- ①如果类成员变量有初始值,用这些初始值初始化成员变量
- ②如果类成员变量没有初始值,则默认初始化该成员变量
class Student { public: int age = 18; long id = 217060; std::string name; }; int main() { /*执行默认构造函数 age、id、name分别被初始化为18、217060、空字符串 */ Student stu; return 0; }
- 使用注意事项:
- 当类中存在内置类型或复合类型(比如数组和指针)或管理动态内存的类时,必须手动定义构造函数,不能使用默认的构造函数。否则,用户创建的类的对象就会得到未定义的值
default构造函数(C++11标准)
- 设计目的:我们知道当一个类中手动给出构造函数时,默认的构造函数就会消失。C++11标准定义,如果当类中给出构造函数时,我们还需要类的默认构造函数,那么就可以在构造函数后面给出“=default”,这样这个构造函数的作用就等同与默认构造函数
语法:
- =default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部
- 和其它函数一样,如果=default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的
- 重点:=default的默认构造函数必须遵守默认构造函数的规则,如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数“初始值列表”来初始化类的每个成员(见下面的案例)
//这个默认构造函数之所以对Student有效,是因为我们为内置类型的数据成员提供了初始值 //如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表来初始化类的每个成员 class Student { public: Student()=default; Student(int age, int id) { this->age = age; this->id = id; } public: int age = 18; long id = 217060; std::string name; }; int main() { Student stu; //相当于执行了“Student()=default”构造函数,也相当于执行了类的默认构造函数 return 0; }
二、析构函数演示案例
class Cat { private: int m_Feet; int m_Ear; pubic: Cat(); Cat(int a,int b); }; int main() { Cat oCat();//可能会出现错误,被误系统认为一个函数 Cat oCat;//调用无参构造 Cat oCat(1,2);//调用有参构造 Cat oCat=Cat();//调用无参构造 Cat oCat=Cat(1,2);//调用有参构造 Cat* pCat=new Cat;//调用无参构造 Cat* pCat=new Cat();//调用无参构造 Cat* pCat=new Cat(1,2);//调用有参构造 }
注意事项
- ①在上面的实例中 Cat oCat=Cat(); 其实为拷贝构造:即=号后面构造一个临时对象,然后拷贝给oCat,拷贝构造完成后,临时对象析构消失
- ②在上面的实例中 Cat oCat();可能会被系统误认为一个函数(返回值类型为Cat,函数名为oCat),因为我们建议在调用无参构造函数时,不建议加上括号。请看下面代码
Cat oCat();//不安全的初始化,可能会被误认为声明一个函数 Cat oCat; //安全的初始化
概念(作用):
- 用来对对象进行善后处理(指针释放、内存释放等...)
特点:
- 析构函数无返回值,函数名:~类名,必须存在于public中
- 析构函数只有一个,不能重载
- 析构函数不可以主动调用,等到对象被释放时,系统自动执行析构函数
- 与构造函数,析构函数不用来销毁static数据成员
- 与构造函数相反,构造函数初始化数据成员的顺序是按照数据成员在类中出现的顺序进行初始化的;析构函数执行时,按照成员初始化顺序的逆序销毁
什么时候调用析构函数:
演示案例:
{ Sales_data *p=new Sales_data; auto p2=make_shared<Sales_data>(); Sales_data item(*p); vector<Sales_data> vec; vec.push_back(*p2); delete p; } //退出局部作用域时:对item、p2、vec调用析构函数 //销毁p2会递减其引用计数;如果其引用计数变为0,对象被释放
默认析构函数(合成析构函数)
- 如果没有在类中给出析构函数,编译器自动生成一个默认的析构函数(无参数、函数体为空)
- 当手动给出析构函数时,默认的消失
- 当类中有动态内存的变量时,不能使用默认析构函数,必须手动设计析构函数
- 对于某些类,合成析构函数被用来阻止该类型的对象被销毁(见阻止阻止拷贝部分);如果不是这种情况,合成析构函数的函数体为空
class Sales_data{ public: ~Sales_data(){} int a; string s; } //Sales_data类定义的对象被销毁时,成员会被自动销毁,其中string的析构函数会被调用
智能指针成员的销毁:
- 与普通指针不同,智能指针数据成员是类类型,所以自己具有析构函数。因此智能指针成员在析构阶段会自动销毁,不需要手动销毁
使用=default
- 与构造函数使用=default一样,析构函数也可以使用=default
- =default的析构函数就相当于系统默认的析构函数
- 当在类内使用=default时,函数将隐式地声明为内联,如果不希望是内联函数,就将函数在类外定义
class Sales_data{ public: ~Sales_data()=default; }
演示案例
class Cat { private: char* name; public: ~Cat(); }; Cat::~Cat() { if(name)//先判断成员是否为空 { delete[] name; name=nullptr; } }
- 注意事项:释放带有内存的成员时,需要先判断成员变量是否为空。如果不为空则释放,如果为空则不释放
需要析构函数的类也需要拷贝和赋值操作
- 原则:通常,如果一个类需要一个析构函数,我们几乎可以肯定这个类也需要一个拷贝构造函数和一个拷贝赋值运算符
- 案例:
- 下面有一个类和一个函数:函数是传值参数,因此参数会拷贝
class HasPtr{ public: HasPtr(const std::string &s=std::string()):ps(new std::string(s)),i(0){} ~HasPtr(){delete ps;} private: std::string *ps; int i; } HasPtr f(HasPtr hp) { HasPtr ret=hp; //拷贝给定的HasPtr return ret; //ret和hp被销毁 }
需要拷贝操作的类也需要赋值操作,反之亦然