文章目录
第一部分-类与对象的基础铺垫
1. C++中类的引入
c++中的类虽然与c语言的结构体有许许多多的相似之处,但是还是有巨大的不同的。因为c++的类中不仅可以定义变量,还可以定义函数,而c语言的结构体只能定义变量。
//c
struct A
{
int _a;
int _b;
};
//c++
class A
{
int _a;
int _b;
bool comapre(int x, int y)
{
return x < y;
}
};
2. 类的定义
2.1 类定义的基本知识
类的定义可以用两种关键字:class, struct(但是c++更爱用class)。他们两个的区别就是:
- class默认成员和属性全是private
- struct默认成员和属性全是public
(private,public具体是什么在后边有所讲解)
class className
{
//类体:由成员函数和成员变量组成
}; //一定要注意后面的分号
className为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数成为类的方法或者成员函数。
2.2 类的两种定义方式:
方式一:声明和定义全部放在类体中。注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。`
class Person
{
public:
void showInfo()
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
char* _name;
char* _sex;
int _age;
};
方式二:类的声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数在实现时需要指明函数属于的类域
//.h文件
class Person
{
public:
void showInfo();
char* _name;
char* _sex;
int _age;
};
//.cpp文件
#include "person.h"
void Person::shoeInfo() //必须在此处指明函数属于的类域
{
cout << _name << "-" << _sex << "-" << _age << endl;
}
一般情况下在工程当中采用的是第二种。但是平时大家写的时候一般采用第一种。
2.3 定义时成员变量的命名规则
首先观察一段代码:
class Date
{
private:
int year;
int month;
int day;
public:
bool equal(int year)
{
return year == year;
}
};
那么这里对比的是类的成员变量和函数的形参么,这个year又该如何区分呢?
所以就给我们带来一些问题。因此正确的命名规范就是类的成员变量在名字前多加一个_来和成员函数的形参进行区分(当然只要可以区分其他方式也可以,看公司要求)。
class Date
{
private:
int _year; //正确的命名规范
int _month;
int _day;
public:
bool equal(int year)
{
return _year == year;
}
};
3. 类的访问限定符及封装
3.1 访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户(在类内不起作用)使用。
说明:
1. protected和private虽然都是在类外不能直接访问,但是它们两个仍有区别。具体的在继承部分进行讲解。
2. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符的出现时为止,如果后面没有访问限定符,作用域就到}即类结束。
3. class默认权限private,struct默认权限是public(因为要兼容C)。
注意:访问限定符只在编译时有用,当数据映射到内存之后,没有任何访问限定符上的区别。
思考:C++中struct和class的区别是什么?
3.2 私有成员的访问问题
在类的方法里,不仅可以访问当前类对象的私有成员,也可以访问外边传过来的对象的私有成员变量,但传过来的类必须和当前类是同一个类。如果不是同一个类的对象则无法访问。
class A
{
public:
int add(const A& a)
{
return a._x + b._y; //这样是可以的,因为是同一个类,相当于在类内
}
private:
int _x;
int _y;
};
class B
{
public:
int add(const A& a)
{
return a._x + b._y; //这样是不可以的,因为不是同一个类,相当于在A类的外部无法访问。
}
};
4. 类的作用域
4.1 类的作用域的基本作用
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::指明成员属于哪个类域。
class Iphone
{
public:
static int price; //声明静态变量
};
int Iphone::price = 10000; //定义时必须加上类域
4.2 类里面的成员先后
在一个类中,类中成员的先后是没有区分的,不像mian函数里的从上向下编译的顺序一样,类中所有的成员是一个整体,没有先后之分。
class Test
{
public:
int sum() //虽然x, y定义在sum函数的下边但是仍然可以运行
{ //因为类是一个整体,没有先后之分
return _x + _y;
}
private:
int _x;
int _y;
};
5. 类的实例化
类实例化出对象就像现实生活当中使用建筑设计图建造出房子,类就像是设计图,只是设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间,单独一个类是没有分配实际的内存空间的。
class Person
{
int _age;
};
int main()
{
Preson zhangsan;
zhangsan._age = 10; //正确的
Person._age = 10; //错误的
}
6. 类对象存储结构以及大小的计算
6.1 类对象的存储结构
class A
{
public:
void print()
{
cout << _a << endl;
}
private:
char _a;
};
上面这个类既有成员变量,又有成员函数,那么这个类的对象在实际存储中是如何存储的呢?是变量和函数一起存储的么?
实际上:每一个对象只保存成员变量,成员函数存放在公共的代码段。
这样做的好处是什么呢?
因为每个对象调用的函数是一样的,所以把函数保存一份放在公共代码区谁用谁调用就会减少空间浪费,避免每一个对象都保存一份相同的代码。
6.2 类大小的计算
class A
{
public:
void print()
{
cout << _a << endl;
}
private:
char _a;
};
class B
{
public:
void f2() {}
};
class C
{
};
那么请问类A和类B和类C的大小分别是多少呢?
因为成员函数没有存储在对象中,而是存储在公共代码区,所以类的大小只计算成员变量的大小,并且需要考虑结构体内存对齐规则。那么如果一个类没有成员变量,那么对象的大小为0么?当然不是,因为一个对象的大小不可能为0,没有成员变量的类的对象的大小是1B。这一个字节不是用来存储数据的,是用来占位的,标识对象被实例化定义出来了。
这里可以额外复习一下结构体内存对齐规则:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
【面试题】
1 .结构体怎么对齐? 为什么要进行内存对齐?
2 . 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
3 . 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
7. this指针
7.1 this讲解
先观察一段代码:
class Date
{
public:
void Init(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 d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
d1和d2调用函数时,仅仅只是把参数传递了过去,传过去以后是如何区分设置d1还是d2对象呢?
这时this指针就来了:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。即:
this指针的特性:
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值,但是this指向的内容可以改变。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,用户不可以自己显示传递。
思考题:
1.this指针存在哪里?
2.this指针可以为空么?
7.2 深度理解this以及对象访问成员
1.观察以下代码
问:运行结果是什么?
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
结果是正常运行。原因是:
这道分析完以后可以自己尝试分析下一道:
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
第二部分-六个“天选之子”
1. 类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
默认成员函数:用户没有显示实现,编译器会默认生成的成员函数称为默认成员函数。
那么空类中什么也没有么?其实任何类什么都不写时,编译器会自动生成以下6个默认的成员函数。
接下来就要重点学习一下这六个天选之子!
2. 拷贝构造函数-初始化
2.1 构造函数的引入
class student_grade
{
public:
void Init(int Math, int Chinese, int English)
{
_Math = Math;
_Chinese = Chinese;
_English = English;
}
void Print()
{
cout << _Math << _Chinese << _English << endl;
}
private:
int _Math;
int _Chinese;
int _English;
};
int main()
{
student_grade stu1;
stu1.Init(99, 89, 89);
stu1.Print();
student_grade stu2;
stu2.Init(100, 82, 95);
stu2.Print();
return 0;
}
对于student_grade类,可以通过 Init 公有方法给对象分数,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
class 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;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
};
注意:如果通过无参的构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 eg: Date d3()
//声明的是函数d3, 该函数无参,返回一个日期类型的对象。
- 如果类中没有显式定义的构造函数,则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 d1;
return 0;
}
针对上述代码有两种分析情况:
(1)因为Date d1调用的是无参构造所以将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
(2)将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器不再生成默认构造函数
那么默认构造函数有什么作用呢?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。编译器生成默认的构造函数会对自定类型成员调用的它的默认构造函数,对内置类型不做处理。
补充:C++11 中针对内置类型成员不初始化的缺陷, 又打了补丁,即:内置类型成员变量在类中声明可以给默认值
class student_grade
{
public:
void Init(int Math, int Chinese, int English)
{
_Math = Math;
_Chinese = Chinese;
_English = English;
}
void Print()
{
cout << _Math << _Chinese << _English << endl;
}
private:
int _Math;
int _Chinese;
int _English;
};
class student
{
public:
private:
int _age = 21; //内置类型可以给默认值
student_grade _grade; //自定义类型不能给默认值
};
- 无参的构造函数、全缺省的构造函数、我们没写编译器默认生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
判断一下这个程序是否可以编译通过:
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;
}
补充:自定义类型的数组会自动调用size次构造函数,来初始化数组中存储的类对象
3. 析构函数-清理资源
3.1 概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数(析构函数不能重载)。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:
//......
~Stack()
{
if (_array)
{
free(_array); //重点在此,释放申请的堆上的空间,因为堆上的空间不会自动释放
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
- 同构造函数一样,编译器默认生成的析构函数会对自定义类型成员调用它的析构函数, 对内置类型成员不做处理(因为内置类型一定是在栈上的数据,所以也会随程序的结束而收回)
class Time
{
public:
~Time()
{
cout << "~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 d;
return 0;
}
综上:
1.也就是说创建哪个类的对象则调用该类的构造函数, 销毁哪个类的对象则调用该类的析构函数。
2.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。若有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
4. 拷贝构造函数-拷贝一个新对象
4.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
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;
Date d2(d1);
return 0;
}
这里拷贝构造函数的参数是const Date& d,而如果是const Date d就会陷入死循环是为什么呢,这里就给大家画图说明一下:
因此如果是传类类型对象的引用的话, 因为传引用并不需要创建一个新的对象,
而是起别名,所以就不会在调用拷贝构造的时候,需要传参用已存在的对象创建新的函数形参对象额外的再次调用拷贝构造函数,因而造成不断地递归…
- 若未显式定义,编译器会生成默认的拷贝构造函数。 内置类型按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。自定义类型调用自己的拷贝构造函数完成拷贝,若自定义类型没有实现拷贝构造函数,则照样进行浅拷贝。
那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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;
}
为什么看似完美的程序最终却会崩呢?接下来给大家解答
总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
那这个拷贝构造函数该如何实现呢?
那么深浅拷贝的区别到底是什么呢?
4.3 拷贝构造函数典型应用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象(引用等其他则不调用拷贝)
- 函数返回值类型为类类型对象(引用等其他则不调用拷贝)
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;
}
观察上述代码,你知道都哪里用到了拷贝构造函数么?
总结:为了提高程序效率,一般对象传参时,尽量使用引用类型,因为减少了拷贝构造的调用。返回时根据实际场景,能用引用尽量使用引用,因为减少了临时变量的创建。
5. 赋值运算符重载
5.1 运算符重载
5.1.1 基本概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
参数列表:operator函数参数在设置时应当按照操作符操作数的顺序从左向右顺序依次将对应操作数作为参数
//比如重载 > 符号
class Number
{
public:
Number(int x)
:_x(x)
{}
bool operator>(const Number& y) //注意运算符重载函数的书写方式
{
return _x < y._x;
}
private:
int _x;
};
int main()
{
Number x(10);
Number y(20);
cout << (x > y) << endl; //重载之后只有当x._x < y._x时才会返回真
//因此运算符重载改变了>符号的行为
return 0;
}
5.1.2 运算符重载的注意事项
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this - .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
现。
5.1.3 运算符重载函数实现的两种方式
方式一:类外实现
// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
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);
cout<<(d1 == d2)<<endl;
}
注意点:
- 当operator函数实现在类外时,是需要访问类内成员的,而类内的成员是私有的,如何才能让类外的函数访问到类内私有的成员呢?有三个方法:
(1) private -> public
(2) 可以在类内写一个get函数
(3)友元函数- d1 == d2 和 operator==(d1, d2) 的汇编指令是完全一样的
- cout << (d1 == d2) << endl; 不能写成cout << d1 == d2 << endl; 因为<< 的优先级是高于==的优先级,这样写的话会报错因为相当于(cout << d1) == (d2 << endl); 就全部乱套了。
因此在用运算符重载后的运算符时,只要遇到<<等时,尽量加上(),防止因为运算符优先级而出错。
方式二:类内实现
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;
};
注意点:
1.为什么调用==时是两个操作数,但是类内的operator在实现时,只有第二个操作数这一个参数,第一个操作数为什么没有传过来呢?这是为什么呢?
其实根据前边的知识就可以想到,因为this指针的存在。 在类内实现operator 则 d1 == d2 等价于 d1.operator==(d2)相当编译器已经默认帮我们把第一个参数的地址给传了过去,然后在函数内通过this指针对第一个参数的成员进行访问。所以类内operator在实现时,第一个操作数(一般是最左边的操作数)永远是编译器帮我们默认传送的(this),因此类内operator参数的个数= 操作数的个数 - 1。
2.类内实现和类外实现的运算符重载函数用操作符方式调用的方式相同,直接显式调用operator函数的方式不同
5.2 赋值运算符重载
5.2.1 赋值运算符重载格式
1.参数类型:const T&,传递引用可以提高传参效率
2.返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
3.检测是否自己给自己赋值
4.返回 *this :要复合连续赋值的含义
注意:
- 任何重载的运算符都有两种调用方式:
方式一:直接调用(调用函数的方式)
方式二:用操作符- 重载运算符时,应当注意这个运算符是否可以连续使用,当可以连续使用时就应当注意返回值的问题,想当返回什么值时可以保证运算符连续使用
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)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
想必大家已经明白为什么返回值是 Date&:
5.2.2 赋值运算符重载的特性
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。所以赋值运算符重载必须定义为类的成员函数。
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,内置类型以值的方式逐字节拷贝。而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值,若自定义类型没有实现对应的赋值运算符重载则也会以值的方式逐字节拷贝。
例子:
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
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;
Date d2;
d1 = d2;
return 0;
}
那么编译器默认生成的赋值运算符重载可以完成字节序拷贝,那么还需要自己来实现么?就不会产生任何问题了么,大家看下面这个程序:
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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;
s2 = s1;
return 0;
}
想必很多人立马想到了拷贝构造那里的问题,不得不说两次析构的确会导致,但是这里还会产生另一个问题:
总结:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
5.2.3 前置++和后置++
大家先看一段代码:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}
注意点:
- 前置++和后置++均是单目运算符,所以在类内实现得运算符重载函数参数为空
- 前置++和后置++在实现运算符重载时,大家会注意到它们俩的形式完全相同,那么该如何对他俩进行区分呢?
- 通过前置++和后置++的实现可以发现,前置++的效率是优于后置++的,因为前置++相比于后置++少两次拷贝构造
- 前置后置–与前置后置++异曲同工之妙
5.3 理解C++中的常用的运算符重载
5.3.1 <<运算符的重载
在C语言中<<运算符是位运算符,表示左移一位,那么为什么在C++中就成为了输出呢?当然是因为运算符重载,在C++中cout就是一个类,而<<就是在cout类内的运算符重载。
那么大家思考一下,自定义类型是否可以将<<运算符重载在类内?
补充: cout是ostream的对象 cin是istream的对象
class Person
{
//其他函数省略
public:
ostream& operator(const ostream& out)
{
out << _name << _age << _weight << endl;
}
private:
char _name[20];
int _age;
int _weight;
};
其实可以也可以,但是一般我们不会这样做,因为:
5.3.2 常用语句的理解
cout << i << endl;
(1)就包含了运算符重载 + 函数重载(因为可以输出int、doube、char等许多类型)。
(2)而且在输出时cout和printf,C和C++的缓冲区同步也会影响效率(但是现在cpu很强,所以对效率的影响基本忽略)
6. const成员
大家想必都知道const变量,那你知道const成员么,没错const也可以修饰类的成员。
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
注意:
- 一个成员函数可以同时是const和非const,因为构成函数重载
- 内部不改变对象属性的成员函数最好加上const,因为这样的话const对象和普通对象都可以调用(成员函数加const时声明和定义都要加上const)
理解图:
示例代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
思考题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
7. 取地址及const取地址操作符重载
class Date
{
public :
Date* operator&() //不写会默认生成
{
return this ;
}
const Date* operator&()const //不写会默认生成
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,因为编译器默认生成的这两个函数就可以满足我们的基本需求。只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
例如:
class Date
{
public :
Date* operator&() //不写会默认生成
{
return nullptr ;
}
const Date* operator&()const //不写会默认生成
{
return nullptr;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这样的话,当对这个类的对象使用取地址运算符时,无论怎样都会返回nullptr,我觉得用来恶搞比较有意思
8. 六个天选之子的简单总结
第三部分 类和对象的补充
1. 再谈构造函数
1.1 构造函数体赋值
初始化就是指在变量定义时赋值称为初始化。 赋值是指变量定义后再进行赋值。
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
1.2 初始化列表
1.2.1 初始化列表的基本概念和用法
那么类对象成员变量的定义和初始化是在哪里进行的呢?
答案就是初始化列表 ,类的成员变量均会在初始化列表中定义,无论是否显式在初始化列表中写明。而且初始化时可以用构造函数的参数,并且若没有在初始化列表写初始化值,则会在定义时用缺省值初始化。
初始化列表形式:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
例子:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
1.2.2初始化列表的注意事项
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
(1)引用成员变量,因为引用变量必须初始化
(2)const成员变量,因为const变量必须初始化
(3)自定义类型成员(且该类没有默认构造函数时),因为没有默认构造时,必须进行显式调用构造函数
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
可以分析一下这个题目:
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
return 0;
}
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
分析:
1.3 explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
Date(int year)
:_year(year)
{}
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
}
分析:
那么如何制止类型转换呢?
2. static成员
2.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
大家来看这道题来促进理解:
面试题:实现一个类,计算程序中创建出了多少个类对象。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
return 0;
}
分析一下咯:
补充:
大家观察这个代码,你觉得会报错么?
class A
{
public:
static int count;
};
int A::count = 1;
int main()
{
A aa1;
cout << A::count;
cout << (aa1.count);
A* ptr = nullptr;
cout << ((*ptr).count);
cout << (ptr->count);
return 0;
}
分析:
所以不会报错,而是正常运行。
2.2 特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
2.3附加的思考题
- 静态成员函数可以调用非静态成员函数吗?
- 非静态成员函数可以调用类的静态成员函数吗?
3.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为:友元函数和友元类
3.1 友元函数
3.1.1 友元函数的引入
问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
3.1.2 友元函数的特性
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
注意事项:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰,因为没有this指针
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2 友元类
友元类所具有的特性如下:
- 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。 - 友元关系不能继承,在继承位置再给大家详细介绍。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
//中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
补充:模板类的友元
template<class T>
class A
{
//...
private:
T _a;
};
class B
{
template<class T> //注意模板类友元声明时需要加上模板参数
friend class A;
//...
private:
int _b;
};
4. 内部类
4.1 概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
4.2 特性
- 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
5. 匿名对象
匿名对象就是没有名字的对象,有如下特性:
- 定义格式:类名() :也就是类名直接调用构造函数
- 匿名对象具有常性
- 匿名对象的生命周期只有一行,就是定义的那一行,定义的下一行就会自动调用析构函数
- 匿名对象在某些特定环境中具有很好的应用,比如有时函数返回对象时需要构造对象,有时函数传参传对象需要构造对象,或者需要调用某个类的成员函数等某些场合下
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
6. 拷贝对象时的一些编译器优化
在传参接收和传返回值接收的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
观察如下代码:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
总结:优化的对象一般是构造和拷贝构造,连续的构造和拷贝构造优化为一个构造,连续的拷贝构造优化为一个拷贝构造。
结束咯!~~