1.构造函数
(1).构造函数的定义
什么是构造函数? 构造函数存在于类中,是类创建对象时,编译器会自动调用构造函数。
只要是类,都会存在一个默认的构造函数。
首先,让我们看看构造函数长什么样子。
class MM{
public:
//无返回值无参
MM(){ };
//无返回值有参
MM(string MMname,int MMage){
name=MMname;
age=MMage;
}
protect:
string name;
int age;
}
如上,构造函数的函数名和类名是相同的,但是构造函数是没有返回值的(这里的没有返回值不是指void),有无形参都行。上述代码就定义了有参和无参的构造函数(一个类中的构造函数可以有多个)。
(2).关于默认的构造函数
在实际的应用中,一般都是需要构造函数的。 如果没有给类定义构造函数,则编译会自动生成一个缺省的构造函数。这个就是默认构造函数,默认构造函数的作用只有创建对象,并不能给数据成员初始化,默认构造函数的作用类似于 MM() { }; 只不过后者是我们写出来的,不是默认构造函数。
class MM{
public:
//可以通过delete关键字来删除默认的
MM()=delete;
protected:
string name;
int age;
}
int main(){
//MM m; 默认的构造函数删除之后就无法正常创建对象了
reutrn 0;
}
C++新标准中,可以通过关键字 delete 来删除类中的函数:MM()=delete; 不过删除了之后必须得重新写一个构造函数,不然没法创建对象。 同时也可以 使用 MM() = default; 可以调用默认的无参构造函数(官方解释:运行速度会比自己写的无参构造函数快)
(3).构造函数缺省
利用构造函数的缺省,可以创建”长相“不同的对象。但是不管构造函数怎么缺省,必须要保证构造函数的形参会被赋值(无论是缺省值还是实参值)
class MM{
public:
//缺省最好是全缺省,防止意外
MM(string MMname=" ",int MMage=20){
name=MMname;
age=MMage;
}
//上面这个函数缺省 等效于下面三个函数
MM() {}
MM(string MMname) { name=MMname; }
MM(string MMname,int MMage) { name=MMname; age=MMage;}
protected:
string name;
int age;
}
int main(){
//利用函数缺省创建对象
MM m;
MM m1("小红");
MM m2("小白",30);
return 0;
}
注意:第一个对象 MM m;千万不能写成MM m(); 前者是创建对象 后者则会被编译器误解为一个函数申明!!!!!
但要注意以下情况:下面代码中 MM的构造函数不是全缺省(MMname没有被缺省),而此时我们想要创建一个MM m;的无参对象。这个是不允许的,构造函数的形参必须要有值传入,无论缺省值还是形参值。而m需要的是一个无参构造函数,MM类中是没有的。所以会报错。
class MM{
public:
MM(string MMname,int MMage=20){
name=MMname;
age=MMage;
}
protected:
string name;
int age;
}
int main(){
//MM m; 不能这样写,会报错
return 0;
}
(4).初始化参数列表
只有构造函数才有,用来初始化数据成员: 构造函数名(参数1,参数2..) : 成员1(参数1) , 成员2(参数2)...{ } 。即将参数1赋值给成员1,参数2赋值给成员2
class MM{
public:
MM(string name,int age) :name(name), age(age){
}
protected:
string name;
int age;
};
初始化参数列表可以有效地避免由构造函数形参名和成员数据名相同而引发的问题。编译器会自动分辨是形参还是成员变量。
通过初始化参数列表的方式,也可以不用形参,直接给后面的括号里传值。初始化参数列表的形参主要就是提供给冒号后面赋值给数据成员,当数据成员不通过形参初始化时,接不接受形参已经无所谓了。
int M_age=19;
string M_name="小红";
class MM{
public:
//通过直接填值给数据成员初始化
MM() :name("小编"), age(20){
}
//通过全局变量给成员数据初始化
MM() :name(M_name), age(M_age){
}
protected:
string name;
int age;
};
(5).委托构造(构造函数调用构造函数)
在类中,构造函数也可以调用构造函数,不过必须通过初始化成员列表的方式来写构造函数
格式: 构造函数名:需要被调用的构造函数名(参数1,参数2)
class MM{
public:
MM(string name,int age):name(name),age(age){
}
//委托构造 调用上面的构造函数构造
MM():MM("小红",20);
protected:
string name;
int age;
};
int main(){
//下面两个是一样的
MM m1("小红",20);
MM m2;
reutrn 0;
}
2.析构函数
(1).析构函数的定义
和构造函数一样,析构函数也是每个类中默认有的,在一个对象生命周期结束前,会自动调用。既然有默认的析构函数,为什么我们要手写析构函数呢?因为默认的析构函数不会自动释放数据成员申请的内存。
(2).析构函数的使用与注意点
析构函数名是 ~类名 的无参无返回值函数,因为无参,所以析构函数不能被重载。当成员数据是指针类型,并且动态申请内存了,就需要析构函数去释放申请的内存。析构函数在对象消亡前会自动调用,不需要手动调用。
class MM{
public:
MM(const char* pstr,int age):age(age){
//申请内存
str=new char[strlen(pstr)+1];
strcpy(str,pstr);
}
//类中申明析构
~MM();
protected:
//char指针 赋值需要先申请内存
char* str;
int age;
}
//类名限定符 类外实现析构
MM::~MM(){
delete[] str;
}
注意:析构函数可以手动调用,但是这样就二次释放同一块内存,程序会崩溃
当我们的对象是new动态申请的时候,我们不去手动释放对象,析构函数是不会自动被调用的
只有我们delete对象,析构函数才会被调用。
class MM{
public:
MM(string name,int age):age(age){
pname=new char[strlen(name)+1];
strcpy(pname,name);
}
~MM(){
delete[] pname;
}
protected:
char* pname;
int age;
}
int main(){
// 不去手动释放m的话 程序是不会调用析构函数的
MM* m=new MM("小红",20);
//释放
delete m;
return 0;
}
3.拷贝构造
(1).拷贝构造函数的定义
拷贝构造的作用,用一个对象去初始化另一个对象。拷贝构造函数其实也是构造函数的一种,拷贝构造的长相和构造函数一样,只不过拷贝构造只有唯一的参数:对对象的引用。
(2).拷贝构造函数的使用
当我们不写拷贝构造函数时,类中会有一个默认的拷贝构造函数(看不见),帮助我们使用对象去初始化另一个对象。
class MM{
public:
MM(string name,int age):name(name),age(age){
}
void print(){
cout<<name<<" "<<age<<endl;
}
protected:
string name;
int age;
};
int main(){
MM m1("小红",20);
//利用默认的拷贝构造来初始化对象
MM m2(m1);
//输出的结果一定是一样的
m1.print();
m2.print();
return 0;
}
这里就是使用默认的拷贝构造函数,来通过一个对象去初始化另一个对象。
我们也可以自己写一个拷贝构造函数,我们自己写的话,默认的拷贝构造函数就不起作用了
class MM{
public:
MM(string name,int age):name(name),age(age){
}
void print(){
cout<<name<<" "<<age<<endl;
}
//自己写的拷贝构造函数
MM(MM& m){
name=m.name;
age=m.age;
}
protected:
string name;
int age;
};
我们来了解一下拷贝构造的显示调用和隐式调用,显式调用就是正常的创建对象时在括号里填入被拷贝的对象,而隐式调用采用的是 “=” ,而且必须在对象创建的时候赋值,在对象创建后再赋值不叫隐式调用。zx
class MM{
public:
MM(string name, int age) :name(name), age(age){
}
void print(){
cout << name << " " << age << endl;
}
//手动创建一个拷贝构造函数
MM(MM& m){
name = m.name;
age = m.age;
cout << "调用了拷贝函数" << endl;
}
protected:
string name;
int age;
};
int main(){
//创建一个对象
MM m("小兰",20);
//显式调用拷贝构造
MM m1(m);
//隐式调用拷贝构造
MM m2=m1;
//没有调用拷贝构造
MM m3("小白",22);
m3=m2;
return 0;
}
注意上述代码,虽然对象 m3 也能被赋值成功,且和 m2 的值一致,但它的过程中是没有调用拷贝构造函式的(涉及到运算符 “=” 的重载)。而 m1 和 m2 的创捷初始化都是调用了拷贝构造。
当函数的形参为对象时,传参时形参也会调用拷贝构造,而当函数的形参是对象的引用时,则不会调用拷贝构造
void print1(MM m){ //相当于 MM m=实参 产生了拷贝本 会调用拷贝构造
}
//不会调用拷贝构造
void print2(MM& m){ //相当于直接引用对象 没有拷贝本产生
}
另外,当创建对象时使用匿名对象来进行初始化时,我们必须给拷贝构造函数的形参加const
因为匿名对象是一个常属性的值,是一个右值。(不加const,在vs2019及以上版本的编译器会报错)
class MM{
public:
MM(string name, int age) :name(name), age(age){
}
void print(){
cout << name << " " << age << endl;
}
//手动创建一个拷贝构造函数
MM(const MM& m){
name = m.name;
age = m.age;
cout << "调用了拷贝函数" << endl;
}
protected:
string name;
int age;
};
int main(){
//匿名对象
MM m3 = MM("匿名", 20); //匿名对象是一个右值 需要cosnt修饰
m3.print();
return 0;
}
小结:
+ 什么时候调用拷贝构造?
+ 当通过一个对象去创建出来另一个新的对象时候需要调用拷贝。
+ 拷贝构造什么时候需要加const修饰参数?
+ 当存在匿名对象赋值操作的时候,必须要const修饰。
4.深拷贝浅拷贝
默认的拷贝构造是浅拷贝,当拷贝过程中不涉及指针操作时,深浅拷贝都是一样的,当拷贝构造中涉及到指针操作时,浅拷贝就会引发一个析构问题。
class MM{
public:
MM(cosnt char* mmname, int mmage=20 ) : age(mmage){
name = new char[strlen(mmname)+1];
strcpy(name,mmname);
}
MM(cosnt MM& m) : age(mmage){
//这是浅拷贝
name=m.name;
//这里是深拷贝
name = new char[strlen(mmname)+1];
strcpy(name,mmname);
}
void print(){
cout << name << " " << age << endl;
}
protected:
char* name;
int age;
};
上述代码的浅拷贝中 name指向的是指向的是 m.name 所指的内存段 在它们生命周期结束时就会调用析构函数来释放name所指的内存段。所以导致了同一个内存段被多次释放,这是不允许的。
5.构造和析构顺序问题
-
普通对象,构造顺序和析构顺序是相反(先创建的对象后调用析构)
-
new出来的对象,delete会直接调用析构函数
-
static对象,当程序关闭的时候,生命周期才结束,所以是最后释放
#include <iostream>
#include <string>
using namespace std;
class MM
{
public:
MM(string name="x") :name(name) {
cout << name;
}
~MM(){
cout << name;
}
protected:
string name;
};
int main()
{
{
MM mm1("A"); //A
static MM mm2("B"); //B 程序关闭时候才死亡,最后析构
MM* p3 = new MM("C"); //C
MM mm4[4]; //xxxx
delete p3; //C delete 直接调用析构
p3 = nullptr;
//xxxxAB
}
//ABCxxxxCxxxxAB
return 0;
}
实现一个简单的string类:myString
#include<iostream>
#include<cstring>
using namespace std;
class myString{
public:
//构造函数
myString(const char* str){
len = strlen(str);
mystr = new char[len + 1];
strcpy(mystr, str);
}
//析构函数
~myString();
//构造拷贝函数
myString(const myString& str){
cout << "被调用了" << endl;
len = str.len;
mystr = new char[len + 1];
strcpy(mystr, str.mystr);
}
void print(){
cout << mystr << " " << len << endl;
}
//转换
char* c_str(){
return mystr;
}
//拼接
char* append(myString str){
char* strAppend = new char[strlen(str.mystr) + strlen(mystr) + 1];
int i = 0;
while (1){
while (mystr[i] != '\0'){
strAppend[i] = mystr[i];
i++;
}
while (str.mystr[i] != '\0'){
strAppend[i] = str.mystr[i];
i++;
}
strAppend[i] = '\0';
break;
}
return strAppend;
}
protected:
char* mystr;
int len;
};
myString::~myString(){
delete[] mystr;
}
int main(){
myString str1("x");
myString str2(str1);
myString str3="ILoveyou";
str2.print();
cout << str3.c_str() << endl;
cout << str2.append(str3) << endl;
return 0;
}