如果一个类中什么成员都没有,简称为空类。如class Date{};
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数 (编译器做的事)
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数

我们写一个数据结构或者写一个普通的类,我们通常会写一个Init函数来进行初始化,一般是把成员设置为nullptr或者0
缺陷:
Init函数初始化针对这一现象,能不能保证对象一定被初始化,并且不用我们调用呢?— 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
注意:构造函数并不是开空间创建,而是定义对象的时候自动完成初始化,并且一定会初始化
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
注意:
Date dclass Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
return 0;
}
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 ,所以如果用户显式定义的构造函数中没有默认构造,就会报错
- 默认构造函数指的是:可以不传参的构造函数。包括三种:
- 全缺省的
- 无参的
- 我们不写编译器默认生成的
- 一个类必须要有默认构造,否则就会报错,并且只能有一个默认构造
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 下面调用默认构造函数,后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1; //调用默认构造函数
return 0;
}
注意:
class Time
{
public:
//默认构造函数
Time(int time = 0)
{
cout << "Time(int time = 0)" << endl;
_hour = time;
}
private:
int _hour;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d1;
return 0;
}


说明:Date调用编译器默认生成的无参默认构造,所以Date里的自定义类型不做处理。 Date中自定义成员_t调用Time类的默认构造,从而初始化为0
- C++11针对内置类型不初始化的缺陷打了补丁:内置类型可以在类中声明的时候给默认值(缺省值)从而完成初始化
class Date
{
private:
// 给缺省值对内置类型初始化
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
- 语法上无参默认构造和全缺省默认构造可以同时存在,但是调用会存在歧义,报错
class Date
{
public:
Date()//1、无参构造函数
{
_year = 2000;
_mouth = 2;
_day = 5;
}
Date(int year=2000, int mouth=2, int day=5)//2、带参构造函数
{
_year = year;
_mouth = mouth;
_day = day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;//error
//无参调用,编译器不知道调用全缺省还是无参的构造函数,存在歧义(编译器报错)
Date d2(2022, 2, 12);//correct
return 0;
}
总结:
对于一般的类,一般都不会让编译器默认生成构造函数,自己写!显式写一个全缺省默认构造即可
对于一些特殊的类,类的成员都是自定义类型,我们不需要写构造函数,利用编译器自动生成的默认构造即可
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作 。
资源指的是:成员中可能存在动态开辟的空间的指针,如果不单独释放,会造成内存泄漏
如:Stack类中会存在int* _a的变量,指向在堆上动态开辟的内存空间
析构函数是特殊的成员函数
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
// 对于Stack类,需要自己写析构函数释放资源
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
}
//析构函数
~stack()
{
free(_a); //释放资源!
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
stack s1;
stack s2(10);
//在return之前,会调用s1和s2的析构函数
return 0;
}

编译器自动生成的默认析构:
对于内置类型不做处理
对于内置类型自动调用它的析构函数
如下利用两个栈实现一个队列:成员变量都是Stack类型
class stack
{
public:
stack(int capacity = 4)
{
cout<<"Stack(int capacity = 4)"<<endl;
}
~stack()
{
cout<<"~Stack()"<<endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
//两个栈实现一个队列
class MyQueue
{
public:
// 默认生成构造函数和析构函数会对自定义类型成员调用他的构造和析构
private:
stack pushST;
stack popST;
};
int main()
{
MyQueue q;
return 0;
}
输出结果:
析构的完整事件:
如果当对象生命周期结束,完整事件如下:
1、调用该类的析构函数,并执行该析构函数的函数体(如果自己定义了)
2、对于其非静态成员,按照类中声明顺序的相反顺序分别调用各自的析构函数(基本类型不做处理)
所以,如果类中存在其他自定义类型的成员,即使自己写了该类的析构函数,执行完函数体之后,还是会自动调用其他自定义成员的析构函数,并且如果有多个自定义类型,后声明的先析构。
注意:
如果在创建对象的时候,想要利用一个已有的对象 创建一个与之一样的新对象,就要用到拷贝构造
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用 。
注意:拷贝构造也是构造函数,是构造函数的重载。只不过该构造的参数比较特殊,是本类型对象的引用
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发
无穷递归调用 ,必须用引用!- 引用一般加const,是为了防止修改原来的对象
- 拷贝构造的写法有两种
Date d(d1);Date d = d1;// 会转化成Date d(d1)
引发无穷递归时的场景:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //构造d1
Date d2(d1); //利用d1构造d2
return 0;
}

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成
拷贝,这种拷贝叫做浅拷贝,或者值拷贝在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
可以观察到,Date类没有写构造函数,d2调用了编译器自动生成的构造函数完成对d1的拷贝,并且会自动调用自定义成员_t的拷贝构造


注意★
关于深浅拷贝
- 对于如Date这样的类,对象中没有开辟空间,利用默认生成的即可,值拷贝就够用了
- 对于Stack这样的类,里面有动态开辟的空间,如果只是浅拷贝(值拷贝),那么就会出现两个指针指向同一块空间,此时就会出现问题
- 一个对象修改会影响另一个对象
- 会析构两次,程序崩溃
//下面的程序就会崩溃!因为浅拷贝!
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}

ps:同一块内存释放两次为什么崩溃?(个人理解)
同一块内存已经释放了,也就是空间还给操作系统了,而归还后的这块空间可能被别的程序使用,而指针还是指向这块空间(野指针),所以再去free,就可能这段可能正在给别人使用的内存给释放,从而造成非法读写内存。而系统无法判断是否这段空间已经被从新分配了,所以一律视为报错
解决方法:
对于Stack这样有动态开辟空间的类,我们要自己实现深拷贝!
拷贝构造发生情景
使用已存在对象创建新对象
Date d(d1);
函数参数类型为类类型对象
//传递实参时会拷贝给实参,
void Func(Date d){}
Func(d1);
//实际发生:Date d = d1;//d1为实参`
函数返回值类型为类类型对象
Date Func()
{
/**/
Date d;
return d;
//返回时 栈帧会销毁 d销毁
//所以会产生临时对象 利用临时对象才能返回
}
类似这样:

//举个例子:
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022, 1, 13);
Test(d1);
return 0;
}

拷贝构造存在较大的消耗,所以为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用
什么是运算符重载?
对于内置类型如int,我们有这样的情况
int a = 10;
int b = 20;
a+=10;
a+=b;
a==b;
a>b;
a-10;
a-b;
// ....
// ....
这些运算符是十分直观而且易理解的,而我们如果想让自定义类型也可以用其中特定的一部分,所以有了运算符重载
如,让Date类的对象d + 一个天数 d + 10
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
赋值运算符重载:让自定义类型对象可以用运算符。转换成调用这个重载函数
区分函数重载:支持函数名相同的函数同时存在
特征:
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表) ,如:
Date& operator +=(Date& d , int day)参数:操作符有几个操作数,它就有几个参数
注意:
.* :: sizeof ?: . 注意以上5个运算符不能重载。运算符重载可以重载成全局,也可重载在类的内部
但是如果重载成全局涉及到一个问题:类成员的访问
所以,一般赋值运算符重载都重载在特定的类内部!
例子:
实现全局的operator ==
// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//设置为public
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//调用operator==
cout << (d1 == d2) << endl;
//实际转化为 cout << operator==(d1,d2)<
}
类内部的operaotr==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
//转化成cout << d1.operator==(d2) << endl;
}
所以对于运算符重载,实际上就是编译器做了更多的事情,我们写运算符形式,编译器会自动转成调用其原来的样子:

编译器会自动转换,我们不用管
赋值和拷贝构造的区别
如果定义一个对象的时候,用另一个对象去构造,那么就是拷贝构造
Date d(d1) 或 Date d = d1
但是如果一个对象已经存在(已经创建好了),再用另一个对象去赋值,这里就是单纯的赋值了
Date d;
d = d1;
赋值运算符的实现
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值重载
Date& operator=(const Date& d)//第一个参数是this,我们看不到
{
//如果自己给自己赋值,直接返回
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};

注意
赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符重载是类的默认成员函数,如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
C++就是这么设计的!如C++prime里所说
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类
型成员变量是直接赋值的,而自定义类型成员变量会去调用它的赋值运算符重载完成赋值赋值运算符同样存在深浅拷贝问题,如果像Stack那样的类的对象要进行赋值,必须要自己实现深拷贝,否则会发生同一个空间析构两次导致崩溃
我们知道,每一个成员函数都有一个隐含的this指针
this指针不能改变,但是this指针指向的内容可以改变。
但是如果我们想用一个const对象调用成员函数的时候,就会出错
比如:
const Date d;
d.print();//error

所以针对这一现象,必须要有const成员函数的存在!但是this指针又是对用户隐藏的,所以如何把this指针修饰为const呢?
在C++中,将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
const加在函数的后面:
void Display() const,如下图是实际的转换

注意
const修饰成员函数的作用:
- 实际修饰this指向的内容,保证成员函数内部不会修改成员变量
- const对象和非const对象都可以调用该成员函数,非const对象调用就是权限的缩小,没问题
注意事项:(传参时权限的放大和缩小)
const对象可以调用非const成员函数
非const对象不可以调用const成员函数
const成员函数内部不能调用其他非const成员函数
非const成员函数内部可以调用其他const成员函数
总结:
除了在函数内部需要修改成员变量的成员函数,大部分成员函数应该给const,这样const对象和非const对象都可以调用
取地址就是要返回对象的地址
对于一般的内置类型,我们可以直接使用
int a = 10;
cout << &a << endl;
对于自定义类型,有时也需要返回地址
class Date
{
public:
//下面两个运算符重载构成函数重载(以参数this的类型区分)
//普通对象调用
Date* operator&()
{
return this;
}
//const对象调用
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
但是这两个取地址函数是默认成员函数,我们不写会自动生成,并且这两个运算符一般不需要重载,使用编译器默认生成的取地址就可以了,只有很特殊的情况才会去重载,比如想让别人获取到指定的内容而不是直接返回真实的地址
比如:
class Date2
{
public:
string operator&()
{
return "想要我地址?没门!";
}
const Date2* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date2 dd;
cout << &dd << endl;
return 0;
}

(开个玩笑)