前言
C++和C语言之间最大的区别就是C++出现了类和对象。C语言是面向过程的,就好比你想吃饭那么你只能自己亲手做饭吃,切菜,下锅,炒菜,出锅这些事都需要自己去完成。但是在C++中是面向对象的,就好比你去店里吃饭,你只管下单,然后复杂的做法过程你全程不管交给厨师去完成,你只管厨师把饭做好端给你吃。这其中就C++就好比把做法的过程封装了起来变成厨师,你想吃饭的时候就可以让厨师去做,而不用你自己去做饭。
类和对象
struct和class
类里定义的函数默认 inline 修饰,成员函数声明和定义分离,要在定义前面用域作用限定修饰符修饰类域。
C++兼容C语言,并且把结构体 struct 升级成了类。类中的变量称为成员变量,还可以定义成员函数。
struct Class
{
//成员函数
void Init()
{};
//成员变量
int a;
int* b;
};
C++增加了 class,class 中也可以定义成员变量和成员函数。
class Class
{
//成员函数
void Init()
{};
//成员变量
int a;
int* b;
};
访问限定符
访问限定符有三种分别是 public(公有),protected(保护)、private(私有)。
public修饰的成员在类外可以之间被访问
protected和private修饰的成员在类外不能直接被访问
class的默认访问权限为private,struct为public
类的实例化
下面定义了一个类 MyClass,这是这个类的声明。那么类里面的那些成员是声明还是定义呐?
class MyClass
{
public:
void MyFunc(int a)
{
cout << a + _b << endl;
}
private:
//成员变量最好使用_作为标识,防止混淆
int _b = 1;
};
在调用这个类的时候会为这个类和里面的成员变量开辟内存空间,这个时候就是类的实例化(对象的定义)。类定义里的成员都是声明,只有在调用类后为这个成员开辟了内存空间,这个时候成员才是定义。
int main()
{
MyClass Class;
Class.MyFunc(2);
return 0;
}
类的大小
C语言中结构体 struct 的大小只算变量大小然后字节对齐(对齐数:编译器默认对齐数与成员大小的较小值)就可以算出,那么C++中类的大小该怎么算呐?毕竟类中还可以定义函数。
int main()
{
MyClass Class;
Class.MyFunc(2);
cout << sizeof(Class) << endl;
return 0;
}
上面的代码 cmd 给出的回复是 4,也就是说类的大小和结构体的大小计算方式应该是一样的。但是类里面的函数哪去了呐?类中的成员函数放到了公共代码区,每次调用函数的时候就去公共代码区里面调用。
没有成员变量的类
class Test1
{
;
};
class Test2
{
int Sun(int a, int b)
{
return a + b;
}
};
int main()
{
Test1 A;
Test2 B;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
return 0;
}
上面两个类都没有成员变量,按之前的类大小计算来说这里的cmd输出应该是0,但事实上cmd输出的是1。这里就要说一下,没有成员变量的类对象需要1比特,这是为了占位,表示对象存在,不存储有效数据。
This指针
class Date
{
public:
void Init(int year = 2024, int month = 2, int day = 28)
{
_year = year;
_month = month;
_day = day;
}
void Printf()
{
cout << "Date:" << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date A;
A.Init();
A.Printf();
return 0;
}
在 A.Printf() 的时候会去调用 Printf 这个函数,但是我们并没有传参函数就自己去调用变量。这是怎么做到的呐?这是因为编译器会对成员函数做一些处理。
void Printf(Date* const this)
{
cout << "Date:" << this->_year << " " << this->_month << " " << this->_day << endl;
}
int main()
{
A.Printf(&A);
return 0;
}
转化为类似上面的代码。省去了我们自己手动去调用类 A,编译器会帮我们去调用这个类。我们不能自己手动显式的去调用,但是我们可以在函数内部显式使用 this 指针。this 指针是形参在函数调用创建栈帧时创建。
void Printf()
{
cout << "Date:" << this->_year << " " << this->_month << " " << this->_day << endl;
}
类的默认成员函数
构造函数和析构函数
构造函数是特殊的成员函数,负责初始化对象。构造函数有以下几个特性:
#1 构造函数的函数名与类名相同
#2 构造函数没有返回值也不需要写void
#3 对象实例化时编译器自动调用对应的构造函数
#4 构造函数可以重载
#5 没有显示定义构造函数,编译器会自己生成一个无参的默认构造函数
一旦用户显式定义了构造函数则编译器将不再生成
#6 编译器默认生成的构造函数,内置类型不做处理,自定义类型会去调用它的默认构造
一般情况下有内置类型的类需要自己写构造函数,全是自定义类型的类可以用编译器的构造函数
#7 C++11针对 #5 打了一个补丁,内置类型可以给缺省值
#8 不传参就可以调用的构造函数就是默认构造函数
析构函数与构造函数功能相反,负责在对象销毁时完成对象中资源的清理工作(防止内存溢出)。析构函数有以下几个特性:
#1 析构函数是在类名前面加上字符"~"
#2 无参数无返回值类型
#3 一个类只能有一个析构函数,如果未显示定义
系统会自动生成默认析构函数,析构函数不能重载
#4 对象生命周期结束时,编译器会自动调用析构函数
#5 编译器默认生成的析构函数,内置类型不做处理,自定义类型会去调用它的默认析构
#6 一般有动态申请空间的变量就需要写析构函数
#include <assert.h>
class SeqList
{
public:
SeqList(int capacity = 4);
~SeqList();
private:
int* _data;
int _size = 4;
};
SeqList::SeqList(int capacity)
{
int* New = (int*)malloc(sizeof(int) * capacity);
assert(New);
_data = New;
_size = capacity;
}
SeqList::~SeqList()
{
assert(_data);
free(_data);
_data = nullptr;
_size = 4;
}
int main()
{
SeqList S1(10);
return 0;
}
拷贝构造函数
拷贝构造函数是特殊的成员函数,它的特性如下:
#1 拷贝构造函数是构造函数的一种重载形式
#2 拷贝构造函数的参数只有一个且必须是类类型对象的引用
使用传值方式编译器直接报错,因为会引发无穷递归调用
#3 若未显式定义,编译器会生成默认的拷贝构造函数
内置类型成员完成值拷贝,自定义类型会调用它的拷贝构造
class SeqList
{
public:
SeqList(int capacity = 4);
~SeqList();
SeqList(const SeqList& S);
private:
int* _data;
int _size = 4;
};
SeqList::SeqList(const SeqList& S)
{
_data = S._data;
_size = S._size;
}
int main()
{
SeqList S1(10);
SeqList S2(S);
return 0;
}
上面这个拷贝构造函数是有问题的,问题在于_data = S._data 这只是一个浅拷贝。这相当于把 S2 的 _data 指向 S1 的 _data 就导致 S1和S2 使用的是同一块空间,没有达成拷贝的目的。
要实现这个类的拷贝构造得使用深拷贝完成。
SeqList::SeqList(const SeqList& S)
{
int* New = (int*)malloc(sizeof(int) * (S._size));
assert(New);
for (int i = 0; i < S._size ; i++)
{
*(New + i) = *(S._data + i);
}
_data = New;
_size = S._size;
}
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型。
函数原型: 返回值类型 operator操作符(参数)
#1 不能通过连接其他符号来创建新的操作符
#2 重载操作符必须有一个类类型参数
#3 用于内置类型的运算符,其含义不能改变
#4 作为类成员函数重载时,形参中隐藏有一个this指针
#5 操作符是几个操作数,重载函数就有几个参数
#6 .* :: sizeof ?: . 这个5个运算符不能重载
//定义日期类
class Date
{
public:
//构造函数
Date(int year = 0, int month = 0, int day = 0);
//析构函数编译器生成
//拷贝构造函数编译器生成
//运算符<重载
bool operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
else
return false;
}
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date D1(2024, 3, 3);
Date D2;
cout << (D1 < D2) << endl;
return 0;
}
赋值运算符重载
#1 默认生成的赋值重载模板
class Class
{
&Class operator=(const Class& a)
{
if (this != &a) //判断是否是自己赋值自己
{
...;
}
return *this; //引用返回是为了连续赋值
}
};
#2 默认生成的赋值重载跟拷贝构造行为一样
内置类型浅拷贝,自定义类型去调用它的赋值重载
#3 赋值运算符只能重载成类的成员函数不能重载成全局函数
(怕与编译器生成的赋值运算函数冲突)
赋值运算符重载主要用于已经存在的两个对象之间复制拷贝,用一个已经存在的对象初始化另一个对象,类似下面这个函数。
class Date
{
public:
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
};
但是赋值运算符还有这样的场景
int a = b = c = 1;
a = a;
这个场景之前的函数就没办法完成,需要改进。
class Date
{
public:
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
};
取地址操作符重载和const修饰的取地址操作符重载
我们通常可以对一个对象进行取地址。当对象是内置类型的时候编译器可以自动识别类型进行取地址,当对象是自定义类型的时候取地址符号 (&) 识别不了,需要我们写一个运算符重载函数去实现这个功能。当然取地址操作符重载和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(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
初始化列表
初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
#1 每个成员变量在初始化列表中只能出现一次(只能初始化一次)
#2 类中包含以下成员,必须放在初始化列表位置进行初始化
# 引用成员变量
# const成员变量
引用和const都是必须在定义的时候初始化
# 自定义类型成员且该类没有默认构造时
#3 初始化列表的初始化顺序是在类中的声明次序
与其在初始化列表中的先后次序无关
class Test2
{
public:
Test2(int a)
{
_a = a;
}
private:
int _a;
};
class Test1
{
public:
Test1(int a, int& b, int c, int test)
:_a(a)
, _b(b)
, _c(c)
, _test2(test)
{}
private:
int _a;
int& _b;
const int _c;
Test2 _test2;
};
自定义类型的隐式类型转换
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A a(1);
A b = 1; //隐式类型转换,整形转换为自定义类型
return 0;
}
(A b = 1;) 这一条命令中发生了隐式类型转换,先使用"1"作为参数调用构造函数,再使用拷贝构造拷贝给b。但是通常编译器会在这里进行一个优化,(A b = 1) 就相当于直接调用的构造函数,不再去拷贝了。
explicit
class A
{
public:
explicit A(int a)
:_a(a)
{}
private:
int _a;
};
在构造函数前使用 explicit 修饰就可以不允许这个类进行隐式类型转换。