目录
1. 类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
c++中struct 可以做结构体,也可以做类。
struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名 ::
2. 类的实例化
用类类型创建对象的过程,称为类的实例化。
对象未创建时,类不占空间。创建后,类占空间,类对象也占空间。类对象相当于房子,类相当于图纸。
3. 类对象的存储方式
- 类中仅有成员函数,或为空类:类大小为1字节,分配1byte,不存储数据,只是占位,表示对象存在过
- 有成员变量的类的大小计算方法和结构体大小计算方法一样
结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么要内存对齐:提高编译器数据读取的效率,因为编译器是4个字节,4个字节地读取数据的
4. this指针
其实是Data* const this(this指针不可更改,指向内容可更改)
- this在实参和形参位置不能显示写,但是在类里面可以显示的用,例如:可以在类函数内使用和打印 this。
- this指针是一个形参,一般存在栈帧里,VS一般会用ecx寄存器直接传递
- 类函数的调用:
dl.Init(1,1,1); //== Init(&dl,1,1,1);
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
(*p).Print(); //都运行成功,因为编译器非常聪明,看到Print()函数不存在类对象中,就不会进行解引用,当作传参。
//其实相当于是Init(p,1,1,1);该函数只是打印函数,不含_a,也不是解引用
return 0;
}
5. 默认成员函数
我们不写,编译器自动生成。
6. 构造函数
是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
6.1 特征
其特征如下:
- 函数名与类名相同。
- 无返回值,且不需要写void
- 构造函数可以重载(本质是可以写多个构造函数,提供多种初始化方式)
- 对象实例化时编译器自动调用对应的构造函数。
6.2 默认构造函数
默认构造函数:不用传参就可以调用的构造函数
三种类型:
- 无参的构造函数
- 全缺省的构造函数
- 编译器默认生成的构造函数
但是默认构造函数只能存有一个,多个并存会导致调用二异性。
6.3 全缺省的构造函数的使用
全缺省的构造函数的使用:不能加(),防止对象的定义和函数的声明混淆。(对象定义和初始化是同一个语句)
class Date
{
public:
Date(int year=1,int month=1,int day=1 )
{
_year = year;
_month=month;
_day= day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //d1的_year=1,_month=1, _day=1;
return 0;
}
6.4 编译器生成的默认构造函数
构造函数,也是默认成员函数,我们不写,编译器会自动生成
编译器生成的默认构造函数的特点:
- 我们不写才会生成,我们写了任意一个构造函数就不会生成了
- 内置类型的成员会给个随机值(C++11,声明支持给缺省值)
- 自定义类型的成员回去调用这个成员的默认构造函数
总结:一般情况都需要我们自己写构造函数,决定初始化方式;
成员变量全是自定义类型,可以考虑不写构造函数
7. 析构函数
作用:不是销毁对象,当栈帧结束,对象会自动销毁,而是对象在销毁时会自动调用析构函数,完成对象中资源的清理工作(相当于Destroy函数)。
- 先定义的对象,后销毁,后调用析构函数。(栈:先进后出)
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 内置类型的成员不会处理,自定义类型的成员也会处理回去-----调用自定义类型成员的默认构造函数
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
什么时候析构函数需要自己写呢?
当在堆上开了空间,就需要自己写析构函数若没在堆开空间,则无需写析构函数。因为堆上的空间没法随栈的销毁而释放。
8.拷贝构造函数
8.1 拷贝
拷贝不等于赋值:
- 拷贝构:一个已经存在的对象去初始化另一个要创建对象
- 赋值:两个已经存在对象进行拷贝
c++规定:内置类型的拷贝都是浅拷贝,自定义类型的拷贝都是深拷贝,深拷贝用的是拷贝构造函数。
8.2 浅拷贝
又叫值拷贝。如下,就是将a拷贝给了x。是值的拷贝。
int Add(int x)
{
return x;
}
int main()
{
int a = 1;
Add(a);
return 0;
}
类的浅拷贝也是一样的。如下:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
// 内置类型
int _year;
int _month;
int _day;
};
void func1(Date d) //d1拷贝给了d。
{
d.Print();
}
int main()
{
Date d1(2023, 7, 21);
func1(d1);
return 0;
}
上面两例都不会出错,但是下面栈的例子会报错。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data) //栈的增加数据的函数
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_array); //将_array指向的堆的空间释放
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
void func2(Stack s) //值拷贝,所以s,s1的_capacity,_size和_array都是一样的
{ //类对象s在这个函数栈结束时,也会销毁,就会自动调用s的析构函数
s.Push(1); //所以_array指向的空间会被销毁
s.Push(2);
}
int main()
{
Stack s1;
func1(s1);
return 0; //此时,s1销毁,调用s1的析构函数,_array指向的空间被释放第二次,出现错误
}
解决措施一:改为引用,直接使用本尊。
//只有改变一点点
void func2(Stack& s)
{
s.Push(1);
s.Push(2);
}
坏处:本尊会被改变。
解决措施二:c++的拷贝构造函数
8.3 拷贝构造函数的特点(深拷贝)
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
8.4 拷贝构造函数 无引用–>无穷递归
若拷贝构造函数无引用,如下面的情况:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data) //栈的增加数据的函数
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_array); //将_array指向的堆的空间释放
_array = nullptr;
_size = _capacity = 0;
}
Stack(Stack s)
{
cout << "Stack(Stack& s)" << endl;
// 深拷贝
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_array, s._array, sizeof(DataType) * s._size);
_size = s._size;
_capacity = s._capacity;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s1;
Stack s2(s1); //将s1拷贝给了s2,拷贝构造函数是构造函数的一种重载形式
return 0;
}
上面没有用到引用的代码会有无穷递归问题。
因为 Stack(Stack s) 中的s其实编译器自动进行的一次拷贝得来的。(拷贝方法就是上面代码的方法,就是说又会需要一个s,又要拷贝)
8.5 拷贝构造函数的形式
//相对于上面代码,加个引用就好了
Stack(const Stack& s) //加个const防止s被改
{
...
}
...
Stack s1;
Stack s2(s1); //将s1拷贝给了s2,拷贝构造函数是构造函数的一种重载形式
//Stack s2=s1; 这样也是一样的
8.6 编译器生成的默认拷贝构造函数
我们不写,编译默认生成的拷贝构造,跟之前的构造函数特性不一样
- 内置类型, 值拷贝
- 自定义的类型,调用他的拷贝
- 总结:Date不需要我们实现拷贝构造,默认生成就可以用
Stack需要我们自己实现深拷贝的拷贝构造,默认生成会出问题
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
int main()
{
Date d1(2023, 7, 21);
Date d2 = d1;
/*
Stack st1;
Stack st2(st1);
*/
// MyQueue对于默认生成的几个函数非常受用
MyQueue mq1;
MyQueue mq2 = mq1;
return 0;
}
9. 赋值重载函数
9.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
函数名字为:operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
- 不可以改变操作符的操作数个数,一个操作符有几个操作数,那么重载时就有几个参数,且操作数的顺序与参数顺序一致。
牛客题目:日期累加
9.2 赋值重载函数
赋值重载函数也是默认成员函数。
不写,默认生成赋值重载函数:
- 内置类型值拷贝
- 自定义类型调用赋值重载函数
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
/*Date& operator=(Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._month;
}
return *this;
}*/ //可以省略,因为编译器会自动生成,且生成的函数符合条件
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1,1,1);
Date d2(2, 2, 2);
d2 = d1;
d2.Print();
return 0;
}
对于Date类型,赋值重载函数可以不写,编译器生成的赋值重载函数,符合条件。
对于Stack类型,赋值重载函数要自己写,因为编译器生成的会将 指针a 也赋值,使两个栈指向同一个空间。
9.3 前置++和后置++重载
#include<iostream>
using namespace std;
class Date
{
public:
...
// ++d1 -> d1.operator++()
Date& operator++();
// d1++ -> d1.operator++(0)
Date operator++(int); // 加一个int参数,进行占位,跟前置++构成函数重载进行区分
// 本质后置++调用,编译器进行特殊处理
Date& operator--();
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
//前置++:返回值为原值,*this++;
Date& Date::operator++()
{
*this += 1;
return *this;
}
//前置++:返回值为++后的值,*this++;
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
int main()
{
Date d1(1,1,3);
d1--;
--d1;
return 0;
}
9.4 const 成员函数
关于const,总而言之,就是
- Date前加了const,函数定义声明后一定都要加const。(权限的平移)
- Date没加const,函数也可加const,表示*this的成员不能改。(权限的缩小)
class Date
{
public:
...
void Print() const;
private:
int _year;
int _month;
int _day;
}
//void Date::Print(const Date* this)
void Date::Print() const //这个const保护this指向的内容,*this在函数中不能变
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
int main()
{
Date d1(1,1,3);
d1--;
--d1;
Date d2(1,1,3);
d2.Print(); //权限的缩小
const Date d3(1,1,3);
d3.Print(); //权限的平移 调用时不加const
return 0;
}
- void Print() const; 和 void Print(); 可同时存在; 因为两函数为重载函数,编译器会按d的类型来判断调用哪一个。
void Print() const; //1号
void Print(); //2号
...
Date d; d.Print(); //1,2号都可以,若两个都存在,则编译器选择2号,因为2号更匹配
const Date d; d.Print(); //1号
- 只读函数,加const,防止被改
10. 取地址操作符重载
取地址操作符重载是 默认成员函数 。
class Date
{
public:
...
const Date* Date::operator&() const;
Date* Date::operator&();
private:
int _year;
int _month;
int _day;
}
const Date* Date::operator&() const //适用于读
{
return this;
}
Date* Date::operator&() //适用于读/写
{
return this;
} //两函数可不写,因为这是默认成员函数。
int main()
{
Date d;
cout<<&d<<endl;
return 0;
}
取地址操作符重载函数在99%情况下可以不写,但有一种情况要写:当你不想被取到有效地址时。(这时可以写下面的取地址操作符重载函数)
const Date* Date::operator&() const //适用于读
{
return nullptr;
}
Date* Date::operator&() //适用于读/写
{
return nullptr;
}