目录
1 类的6个默认成员函数
2 构造函数
2.1 显式定义构造函数
2.2 构造函数重载
2.3 构造函数重载的时的坑
2.4 构造函数调用的时候做了什么事
2.5 类定义的时候可以给内置类型成员变量缺省值
3 析构函数
3.1 显式的定义析构函数
4 构造析构的调用过程
5 存在多个类对象时,构造和析构的顺序
6 拷贝构造函数
6.1 拷贝构造函数注意事项
6.1.1 为什么拷贝构造函数不能使用值传递,一定要使用引用传递
6.1.2 如果不改变源对象的值最好使用const进行修饰,防止意外修改
6.1.3 我们不写拷贝构造函数,编译器会给我们个默认的
6.1.4 浅拷贝
6.1.5 浅拷贝的坑
7 赋值重载函数
1 类的6个默认成员函数
如果我们创建了一个空类,编译器会自动生成6个默认成员函数。
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值重载函数
- 普通对象取地址
- const对象取地址
如果我们创建了一个空类,编译器会自动生成6个默认成员函数。
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值重载函数
- 普通对象取地址
- const对象取地址
2 构造函数
构造函数的特性:
- 构造函数,没有返回值也不写void
- 函数名与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象的时候会自动调用构造函数,无需程序员手动调用,而且只调用一次
- 如果程序员没有自己写构造函数,那么编译器会提供一个默认构造函数,编译器提供的构造函数是空实现
- 编译器使用自己的默认构造函数时对内置类型的成员变量不做处理,对自定义类型会自动调用这个自定义类型的构造函数
2.1 显式定义构造函数
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()//显式的构造函数
{
m_year = 2022;
m_month = 6;
m_day = 27;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1;
d1.print();
return 0;
}
构造函数的特性:
- 构造函数,没有返回值也不写void
- 函数名与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象的时候会自动调用构造函数,无需程序员手动调用,而且只调用一次
- 如果程序员没有自己写构造函数,那么编译器会提供一个默认构造函数,编译器提供的构造函数是空实现
- 编译器使用自己的默认构造函数时对内置类型的成员变量不做处理,对自定义类型会自动调用这个自定义类型的构造函数
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()//显式的构造函数
{
m_year = 2022;
m_month = 6;
m_day = 27;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1;
d1.print();
return 0;
}
其中Date()就是我们定义的一个显式的构造函数,这个构造函数作用是初始化了三个成员变量,对于构造函数我们可以理解为,在类的创建的时候想让这个类做什么。
运行结果:
2.2 构造函数重载
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
Date()
{
m_year = 2022;
m_month = 5;
m_day = 27;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1;
Date d2(2022, 6, 27);
d1.print();
d2.print();
return 0;
}
定义了一个有参构造函数,一个默认构造函数,通过不同创建对象的方式,可以调用不同的构造函数。
- Date d2(2022,6,27)调用的是有参构造函数
- Date d1调用的是无参构造函数
定义了一个有参构造函数,一个默认构造函数,通过不同创建对象的方式,可以调用不同的构造函数。
- Date d2(2022,6,27)调用的是有参构造函数
- Date d1调用的是无参构造函数
输出结果:
2.3 构造函数重载的时的坑
我看们看看构造函数重载有什么样的坑,构造函数可以重载,我们如果定义一个全缺省的构造函数呢,这样方便我们使用,不传参的时候就用默认参数。
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date(int year = 2022, int month = 6, int day = 27)
{
m_year = year;
m_month = month;
m_day = day;
}
Date()
{
m_year = 2022;
m_month = 5;
m_day = 27;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1;
d1.print();
return 0;
}
我看们看看构造函数重载有什么样的坑,构造函数可以重载,我们如果定义一个全缺省的构造函数呢,这样方便我们使用,不传参的时候就用默认参数。
我们在创建对象的时候编译器报错了,说包含多个默认构造函数,什么是默认构造函数,就是可以不传参就可以调用的,我们刚才是函数重载啊,我们仔细看看。我们发现Date 创建对象不传参Date(int year = 2022, int month = 6, int day = 27)和Date()这两个函数都可以调的动,于是编译器就不知道该调哪一个了,所以我们在平时写构造函数的时候一定要注意全缺省的构造函数。
2.4 构造函数调用的时候做了什么事
class A
{
public:
A()
{
std::cout << "A的构造函数调用" << std::endl;
}
};
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()
{
}
private:
int m_year;
int m_month;
int m_day;
A a;
};
int main()
{
Date d1;
d1.print();
return 0;
}
输出结果:
调试结果:
我们发现他竟然调用了A的构造函数,而且Date中的成员变量是随机值,于是我们总结出来:
编译器自动生成的默认构造函数是不会对类中的内置类型的成员变量初始化,而类中的自定义类型会调用自定义类型的构造函数
2.5 类定义的时候可以给内置类型成员变量缺省值
这样赋给内置类型缺省值,弥补了C++构造函数没有初始化内置类型的问题
3 析构函数
析构函数特性:
- 析构函数,没有返回值也不写void
- 函数名与类名相同,但是需要在类名的前面加上~
- 析构函数不可以有参数,因此析构函数不可以发生重载
- 程序在对象销毁前会自动调用析构函数,无需手动调用,而且只会调用一次
- 编译器使用析构函数时对内置类型的成员变量不做处理,对自定义类型会自动调用这个自定义类型的析构函数
3.1 显式的定义析构函数
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()
{
std::cout << "Date的构造函数调用" << std::endl;
}
~Date()
{
std::cout << "Date的析构函数调用" << std::endl;
}
private:
int m_year = 2022;
int m_month = 6;
int m_day = 27;
};
void Test01()
{
Date d1;
}
int main()
{
Test01();
return 0;
}
我们定义了Date类的析构函数~Date(),在Test01()函数内创建了一个类,当函数结束的时候这个d1也就销毁,我们看一下会不会调用析构函数。我们发现调用了析构函数,说明类在销毁的时候会自动调用析构函数,我们也可以理解成析构函数就是类结束的时候你想让他做点啥,是不是这样说有点感动。类真是至死不渝,到死都在为你做事。
析构函数特性:
- 析构函数,没有返回值也不写void
- 函数名与类名相同,但是需要在类名的前面加上~
- 析构函数不可以有参数,因此析构函数不可以发生重载
- 程序在对象销毁前会自动调用析构函数,无需手动调用,而且只会调用一次
- 编译器使用析构函数时对内置类型的成员变量不做处理,对自定义类型会自动调用这个自定义类型的析构函数
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()
{
std::cout << "Date的构造函数调用" << std::endl;
}
~Date()
{
std::cout << "Date的析构函数调用" << std::endl;
}
private:
int m_year = 2022;
int m_month = 6;
int m_day = 27;
};
void Test01()
{
Date d1;
}
int main()
{
Test01();
return 0;
}
我们定义了Date类的析构函数~Date(),在Test01()函数内创建了一个类,当函数结束的时候这个d1也就销毁,我们看一下会不会调用析构函数。我们发现调用了析构函数,说明类在销毁的时候会自动调用析构函数,我们也可以理解成析构函数就是类结束的时候你想让他做点啥,是不是这样说有点感动。类真是至死不渝,到死都在为你做事。
4 构造析构的调用过程
我们使用调试详细看看析构函数和构造函数的调用过程,因为不会弄成动图,大家将就看看
我们使用调试详细看看析构函数和构造函数的调用过程,因为不会弄成动图,大家将就看看
Test01函数调用,创建类对象
类对象创建,调用构造函数
如果成员变量有缺省值,构造函数会初始化这些成员变量
然后再打印构造函数的调用
函数结束前
调用析构函数
整个过程大致是这样子的,大家可以自己调试看看。
5 存在多个类对象时,构造和析构的顺序
class A
{
public:
A()
{
std::cout << "A的构造函数调用" << std::endl;
}
~A()
{
std::cout << "A的析构函数调用" << std::endl;
}
};
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()
{
std::cout << "Date的构造函数调用" << std::endl;
}
~Date()
{
std::cout << "Date的析构函数调用" << std::endl;
}
private:
int m_year = 2022;
int m_month = 6;
int m_day = 27;
A a;
};
void Test01()
{
Date d1;
}
int main()
{
Test01();
return 0;
}
输出结果:
我们可以得到,如果类中有类对象时,构造时先调用自定义类型的构造函数,那为什么析构是反着的呢,我们看下面这个图,类A是先创建的,压到了栈里面,后来创建了Date类,也压到了栈里面,我们知道栈的特性是先进后出,所以呢Date先析构然后再是A析构
6 拷贝构造函数
我们如果想要用一个类对象初始化另外一个类对象,这时候就需要使用到拷贝构造函数,拷贝构造函数本质上构造函数的一个重载。
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()
{
std::cout << "Date的构造函数调用" << std::endl;
}
Date(Date&d1)//拷贝构造函数
{
std::cout << "Date的拷贝函数调用" << std::endl;
//用d1初始化
}
private:
int m_year = 2022;
int m_month = 6;
int m_day = 27;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
我们如果想要用一个类对象初始化另外一个类对象,这时候就需要使用到拷贝构造函数,拷贝构造函数本质上构造函数的一个重载。
运行结果:
6.1 拷贝构造函数注意事项
- 定义拷贝构造函数不能使用值传参,要用引用传参,不然会出现死递归
- 定义拷贝构造函数,如果不修改原对象内容的话最,好用const修饰引用,防止被拷贝对象的内容被修改
6.1.1 为什么拷贝构造函数不能使用值传递,一定要使用引用传递
我们使用值传递的时候,编译器帮我们创建了一个临时变量,这时候相当于创建一个临时变量,Date temp 我们把值传递进来的值传给temp,这时候这也相当于用类对象初始化类对象,所以会调用拷贝构造函数,一来二去的一直调用,因为没有停止条件,拷贝构造函数一直递归下去,所以我们定义拷贝构造函数的时候使用引用传递,引用传递不会创建临时变量进行拷贝,他只是给外面的对象取了个别名。
- 定义拷贝构造函数不能使用值传参,要用引用传参,不然会出现死递归
- 定义拷贝构造函数,如果不修改原对象内容的话最,好用const修饰引用,防止被拷贝对象的内容被修改
我们使用值传递的时候,编译器帮我们创建了一个临时变量,这时候相当于创建一个临时变量,Date temp 我们把值传递进来的值传给temp,这时候这也相当于用类对象初始化类对象,所以会调用拷贝构造函数,一来二去的一直调用,因为没有停止条件,拷贝构造函数一直递归下去,所以我们定义拷贝构造函数的时候使用引用传递,引用传递不会创建临时变量进行拷贝,他只是给外面的对象取了个别名。
6.1.2 如果不改变源对象的值最好使用const进行修饰,防止意外修改
class Date
{
public:
void print()
{
std::cout << m_year << "-" << m_month << "-" << m_day << std::endl;
}
Date()
{
std::cout << "Date的构造函数调用" << std::endl;
}
Date(Date& d1)
{
d1.m_day = m_day;//我们不想改变原来对象的值
}
private:
int m_year = 2022;
int m_month = 6;
int m_day = 27;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
我们看上面的代码,我们只是用d1进行初始化d2,但是由于写的过程中写反了,导致将d1对象的成员变量的值修改了,我们并不是想修改,为了避免这种谜之操作,建议在传参的时候使用const修饰引用,这样如果写反了,修改了源对象编译器会报错。
我们看上面的代码,我们只是用d1进行初始化d2,但是由于写的过程中写反了,导致将d1对象的成员变量的值修改了,我们并不是想修改,为了避免这种谜之操作,建议在传参的时候使用const修饰引用,这样如果写反了,修改了源对象编译器会报错。
6.1.3 我们不写拷贝构造函数,编译器会给我们个默认的
而自动生成的拷贝构造函数,它有什么说法呢:
- 对于内置类型的成员,编译器生成的拷贝构造是值拷贝或者叫做浅拷贝
- 对于自定义类型的成员,编译器会去调用这个成员的拷贝构造函数
6.1.4 浅拷贝
对于内置类型的成员,我们编译器生成的拷贝构造是浅拷贝(也叫做值拷贝),浅拷贝是什么意思呢?浅拷贝其实类似于C语言中的memcpy这个库函数,他是将原对象的成员所占空间的起始地址到结束地址这块空间的内容都拷贝给另一个对象。
而自动生成的拷贝构造函数,它有什么说法呢:
- 对于内置类型的成员,编译器生成的拷贝构造是值拷贝或者叫做浅拷贝
- 对于自定义类型的成员,编译器会去调用这个成员的拷贝构造函数
对于内置类型的成员,我们编译器生成的拷贝构造是浅拷贝(也叫做值拷贝),浅拷贝是什么意思呢?浅拷贝其实类似于C语言中的memcpy这个库函数,他是将原对象的成员所占空间的起始地址到结束地址这块空间的内容都拷贝给另一个对象。
6.1.5 浅拷贝的坑
我们一路怎么来怎么全是坑,别怕成功只是一时的,失败才是主旋律,浅拷贝又怎坑我们了
class Stack
{
public:
Stack(int capacity = 4)
{
m_arr = (int*)malloc(sizeof(int) * capacity);
m_capacity = capacity;
m_top = 0;
}
~Stack()
{
free(m_arr);
m_arr = nullptr;
m_top = m_capacity = 0;
}
private:
int m_capacity;
int m_top;
int* m_arr;
};
void test01()
{
Stack s1;
Stack s2(s1);
}
int main()
{
test01();
return 0;
}
我们一路怎么来怎么全是坑,别怕成功只是一时的,失败才是主旋律,浅拷贝又怎坑我们了
运行结果:
程序崩了,为什么呢?因为是浅拷贝的问题,我们拷贝的s1的m_arr成员和s2的_arr成员指向了同一片空间,在析构时对同一片空间进行了free操作,free了两次,有的同学问了,我都把空间置空了,free(NULL)不是和没操作一样吗?这里要注意的是,我们并没有把那块空间的地址置空我们只是拿不到那块空间的地址了,但是另一个类还是能拿得到所以会free两次,会造成内存的非法访问,导致程序崩溃。在这里我们必须使用深拷贝才不会报错,这个我们以后再细讲。
7 赋值重载函数
我们如何把一个类赋值给另一个类呢?这就用到赋值重载函数了。
如果我们自己不写赋值重载函数,编译器会给我们提供,但是编译器提供的赋值重载函数是浅拷贝的。
class Date
{
public:
void operator=(const Date& d1)
{
m_year = d1.m_year;
m_day = d1.m_day;
m_month = d1.m_month;
std::cout << "我看看这个函数调用了没" << std::endl;
}
private:
int m_year;
int m_month;
int m_day;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
我们如何把一个类赋值给另一个类呢?这就用到赋值重载函数了。
如果我们自己不写赋值重载函数,编译器会给我们提供,但是编译器提供的赋值重载函数是浅拷贝的。
输出结果:
但是上面这个写法许多问题,比如说,不能像赋值运算符一样进行连等,
为了搞的跟赋值运算符一样,我们还需优化一下,我们先看int类型连等赋值是怎么做的
首先c赋值给b,然后在赋值给a,a需要的是一个值所以b=c要返回一个值,我们在上面写的赋值重载函数是没有返回值的,这样我们肯定没法做到连等。
Date& operator=(const Date& d1)
{
if (this != &d1)
{
m_year = d1.m_year;
m_day = d1.m_day;
m_month = d1.m_month;
std::cout << "我看看这个函数调用了没" << std::endl;
}
return *this;
}
引用返回直接返回这个变量的别名,且这里出了函数的作用域,*this的内容还在,用引用返回是最优解!上面我们还进行了一个判断,为了去除自己赋值给自己这样的情况。



