类的定义
在C语言中是使用struct来定义结构体,C++引入了class关键字来定义类。
类中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数。
类的两种定义方式:
1、声明和定义全部放在类体中(注意:成员函数在类中定义,编译器可能会当初内联函数处理)
2、类声明放在.h文件中,成员函数定义放在.cpp文件中(注意:这种分离定义方法在定义成员函数前需要声明这个成员函数的归属 (classname::)
类的访问限定符
public(公有)protected(保护)private(私有)
访问限定符的说明:
public:在类外可以访问
private和protected:只有类中可以访问,在类外不可访问
假如class没有访问限定符,那么就默认为private, struct默认为public(为了兼容C语言)
访问限定符的作用域
一个限定符的作用域是从该限定符到下一个限定符,假如后面没有限定符了,就到花括号结束。
类的实例化
用类类型创建对象,称为类的实例化
在定义类的时候,并没有创建对象,也没有分配空间。就好像定义类只是一个工程图纸,在创建类的时候才是动工建筑。
类对象的存储方式
在类的实例化时才分配内存,那么类中有成员函数和成员变量,它们是如何存储的呢?
当一个类中定义了成员变量时,那么这个类只会存储成员变量。当然这里也得考虑到内存对齐。成员函数是存储在公共代码区内,因为这个函数可能会被不断调用。
当一个类中没有定义成员变量时,称为空类。这个空类的大小是1个字节,编译器根据这一个字节来标识这个空类对象。
内存对齐
为什么要内存对齐?
平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
性能原因:数据结构(尤其是栈)应该尽可能在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说:就是拿空间换时间的做法。
内存对齐的方法
以结构体为例子:
编译器有一个默认对齐数,而对齐数 = min(默认对齐数,类型的大小)
1、第一个成员在结构体变量偏移量为0的地址处
2、其他成员变量要对齐到对齐数的整数倍的地址处
3、总体大小为最大对齐数(每个成员都有一个对齐数)的整数倍
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套的结构体的对齐数)的整数倍。
联合体:
至少是最大成员的大小
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
位段:
位段类型名后和:后,这个整数的单位是byte。类型只能为int, un int, s int。(char也属于整型家族)
修改默认对齐数
#pragma pack(想修改的默认对齐数),当然修改完之后,记得修改回来#pragma pack()。
以上就是计算类对象的大小的方法
补充 offsetof宏实现
offsetof是计算成员变量的偏移量。
宏实现是 #define OFFSETOF(st_type, mem_name) (size_t)&(((st_type*)0)->mem_name)
后面的实现的本质是:取成员变量的地址,强转为size_t。
this指针
所有在类中访问成员函数的时候使用this指针。this指针是被编译器隐藏的,存在每个成员函数定义的第一个形参的位置。
this指针的特性
1、this指针的类型:类类型*const,指向这个类对象,不能赋值
2、只能在成员函数内部使用
3、this指针的本质是成员函数的形参,当对象调用该成员函数时,会隐式传递该对象的地址作为实参。所以对象中不存储this指针。
4、this指针是成员函数的第一个形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户自己传递。
5、this指针是形参,所以存储在栈区。
6、一个对象假如为空,那么指向这个对象的this指针也为空,当没有使用this指针去访问成员变量时,程序不会崩溃。否则就会崩溃。(注意:无论空指针是否解引用都不会编译报错,因为在编译阶段检查不出空指针错误)
类的六个默认成员函数
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数
构造函数
构造函数:名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
也就是说构造函数的主要任务是初始化对象
构造函数的特征:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5、如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了,编译将不会生成。
class Date
{
public:
// 这是一个无参构造函数,对应 Date d1;
// 假如没有显式定义构造函数,那么编译器将会自动生成一个默认的无参构造函数,Date d1; 也是可以的
Date()
{
}
// 这是带参构造函数, 对应 Date d2(2024, 4, 25);
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 d1();这是错误的
// 如果想要通过无参构造函数创建对象时,后面不要加括号,不然就会变成函数声明
// 注意编译器自动生成的默认构造函数是无参的,假如像下面这样像赋值创建一个对象是不行的
Date d2(2024, 4, 25);
return 0;
}
那么构造函数会对变量产生什么效果呢?
C++把类型分为两大类型:内置类型(int/char……) 和 自定义类型。自定义类型就是用户使用class/struct/union等定义的类型。对于内置类型,通过默认的构造函数,不会处理,运行出来还是随机值。对于自定义类型,会调用这个类型的构造函数。
基于这一个缺陷,C++11进行了优化:内置类型的成员变量在类中声明时可以给默认值。
class Date
{
private:
// 内置类型
int _year = 1970;
int _month = 1;
int _day = 1;
};
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
基于使用缺省函数定义构造函数,我们可以在声明时给默认值。可以这样
Date(int year = 2024, int month = 4, int day = 25)
{
_year = year;
_month = month;
_day = 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、类中包含以下成员,必须放在初始化列表位置进行初始化:
(1):引用成员变量
(2):const修饰的成员变量
(3):自定义类型的成员(并且该类没有默认构造函数时)
【note】:默认构造函数是不用传参就可以调用的构造函数,有3种:
1、无参默认构造函数
2、带参全缺省的默认构造函数
3、我们不写,编译器自动生成的默认构造函数
3、尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
4、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
上面那句话读起来不像人话,接下来形象地举个例子:
#include<iostream>
using namespace std;
class Date
{
public:
//1、单参构造函数
//explicit Date(int year) // 加上explicit修饰构造函数,禁止类型转换
/*Date(int year)
:_year(year)
{}*/
//2、虽然有多个参数,但是只需要传一个参数。
//explicit Date(int year, int month = 1, int day = 2) // 加上explicit修饰构造函数,禁止类型转换
Date(int year, int month = 1, int day = 2) // 1 和 2只能存在一个,不然调用构造函数的时候会产生冲突。
:_year(year)
, _month(month)
, _day(day)
{}
//赋值运算符重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024); // 这里是调用构造函数,创建d1对象。
// 你还可以这样,c++11才支持, Date d1 = {1, 2, 3};
// 隐式类型转换
d1 = 2023;
// 这里的2023是一个int类型,d1是自定义的Date类型。
// 这里的底层是,会产生一个临时变量,我们设为temp好了。
// 那么就有: temp(2023); d1 = temp
// 这个temp是临时变量,具有常性。
// 但是修改d1并不会影响到赋值的右值,所以不需要加const
return 0;
}
析构函数
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
注意这里的资源清理是对开辟空间或者进程的清理
析构函数的特征
1、析构函数名是在类名前加一个~
2、无参无返回值类型
3、一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4、对象的生命周期结束时,编译器会自动调用析构函数
假如在创建了一个类型,该类型的成员变量包含了一个自定义的类,当该类型对象的生命周期结束时,也会自动调用包含的这个自定义的类的析构函数。
析构函数调用的先后顺序区分
假如创建两个类,d1和d2。假如先创建d1,后创建d2,那么在两个对象生命周期结束时,会先调用d2的析构函数,然后调用d1的析构函数。其实调用析构函数的顺序是先进后出的,也就是先创建的后析构。这就像是栈结构了
// 这里是对一个栈类型的对象析构,这个类型的成员变量
// private:
// int* _a;
// int _size;
// int _capacity;
~Stack()
{
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对该类型对象的引用(一般加const修饰),用已经存在的类对象创建一个新的对象。
拷贝构造函数是构造函数的重载形式
前面的构造函数,是不传参和传成员变量的参数进行构造。而拷贝构造函数是在创建一个类对象时,传递该类已经创建好的对象。
为什么要传该类型对象的引用呢?
一个函数的形参有三种传递方式:值传递、地址传递、引用传递。
值传递时,函数的形参需要在栈上开辟一块空间,去拷贝实参的对象,但是当这个形参去复制这个实参对象时,又需要拷贝构造,这样会形成无限递归调用拷贝构造函数。打比方说:当传递的不是类类型,而是一个int的整型。当去调用这个函数的时候,这个函数的会为形参在栈上开辟一块空间去拷贝这个实参int类型的值,这样是能实现的。但是为什么类类型不行呢?因为int类型是一个内置类,而对于自定义的类类型,每次形参去拷贝实参的对象时,都需要调用该类的拷贝构造函数。
地址传递时,那么实参要像这样 &d, 而拷贝构造函数要像这样:Date (Date* p),这就是一个构造函数了,而不是拷贝构造函数。
引用传递时,该拷贝构造函数时形参是实参的别名,并不需要再次拷贝构造。
若未显示定义时,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存字节一个一个完成拷贝,这种拷贝称为浅拷贝,也叫值拷贝。
当类中没有涉及资源申请时,不显式定义拷贝构造函数,直接使用默认的拷贝构造函数是可以的。但当涉及到资源申请,必须用户显式定义拷贝构造函数。为什么呢?
比如说一个类中的成员变量在构造时需要申请空间。如果已经创建好了这个类对象,再利用这个对象进行拷贝构造一个新的类对象,那么这个两个类的成员变量都指向一块空间。而这两个对象生命周期结束时,会自动调用析构函数,所以会对这块空间析构两次。
拷贝构造函数典型调用场景
1、使用已存在的对象创建新对象
2、函数参数类型为类类型对象
3、函数返回值类型为类类型对象
// 这里日期类并没有涉及资源申请,所以调用默认拷贝构造函数也可以
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1;
Date d2(d1);
Date d3 = d1; // 注意这也是调用拷贝构造函数
retun 0;
}
赋值运算符重载
运算符重载
函数原型:返回值类型 + operator关键字 + 操作符 + 形参列表
注意:
运算符重载的形参也隐藏了this指针。
有五个运算符不能重载:.* :: sizeof ?: .
// 注意这是类的成员函数。
// 假如在类外定义,那么对于类的成员变量就访问不到了(不是public)
// 假如是public,又会丢失类的封装性
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
赋值运算符重载
参数类型:const T& ,传递引用可以提高传参效率
返回值类型:T&, 返回引用同样也是提高效率,有返回值也是为了达到连续赋值。
检查是否自己给自己赋值
// 拷贝构造函数
// 对应 Date d2(d1) 或者 Date d2 = d1;
// 注意:这里实在定义一个对象时使用的,也就是说,只有一个对象d1是已经创建好了的
// 而下面的赋值运算符重载,是两个对象d1 和 d2都是已经创建好了的
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 对应 d2 = d1; 前提d2和d1都是已经创建好了的对象
Date& operator=(const Date& d) // 这里隐藏了this指针, 并且形参使用引用,不需要再次调用构造函数
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 直接返回引用, 也不需要再次调用构造函数。
// 这里为什么要有返回值呢?
//为了模仿连续赋值, 比如 d1 = d2 = d3;
// 连续赋值是从右向左逐个赋值, 可以分为两步: d2 = d3; d1 = d2;
// 那么就需要一个返回值,这个返回值赋值给d1.
}
赋值运算符只能重载成类的成员函数不能重载成全局函数
当重载成全局函数时,那么类中编译器会自动生成一个默认的赋值运算符函数,这个时候两者就冲突了。
浅拷贝
和拷贝构造函数一样,当涉及资源申请,必须用户自己实现。
前置++和后置++重载
// 前置++
Date& operator++()
{
_day++;
return *this;
}
//后置++
// 为什么前置++返回值是引用, 而后置++却是返回值呢?
// 前置++:先++, 再返回++之后的值。所以直接返回引用即可
// 后置++: 返回的是++之前的结果。所以要在++之前先拷贝到临时变量(temp)中
// 但是当函数结束时,temp的生命周期结束,会自动调用析构,所以只能返回值。
// 因为当出来作用域变量的生命周期结束了,不可以使用引用返回。
Date operator++(int)
// C++规定:后置++在重载时,形参为int,该参数在调用时不传递实参
// 由编译器自动传递
{
Date temp(*this);
_day++;
return temp;
}
取地址及const取地址操作符重载
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
const成员
将const修饰的成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。
当创建了一个const修饰的类,那么下次在调用这个类的成员函数时,会涉及到隐含的this指针,这个this指针也应该是const的。像这样
// 类的成员函数
void Print() // void Print(Date* this)
{
cout << _year << endl;
}
void Print() const // void Print(const Date* this)
{
cout << _year << endl;
}
// 主函数中
Date d2(2024, 4, 29);
d2.Print();
const Date d3(2024, 4, 29);
d3.Print();
const的三种位置
const Date* p1; // -> 修饰指向的内容
Date const* p2; // -> 修饰指向的内容
Date* const p3; // -> 修饰指针本身
//技巧:const在*之前就是指向内容, 在*之后就是指针本身
static成员
概念:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称为静态成员变量;用static修饰的成员函数,称为静态成员函数。静态成员变量一定要在类外进行初始化。
下面我们用一个问题来更好理解static:实现一个类,计算程序中创建出多少个类对象。
class A
{
public:
// 每创建一个类,肯定会调用构造函数,或者拷贝构造
A() // 构造函数
{
n++;
}
A(const A& d) // 拷贝构造
{
++n;
}
~A() // 析构函数
{
--n;
}
static int GetN() // 静态成员函数存储在全局区/数据段
{
return n;
}
private:
static int n;
};
int A::n = 0; // static 修饰的全局变量必须在类外初始化
int main()
{
A d1, d2;
A d3 = d1;
cout << d1.GetN() << endl; // 这三种方式都可以访问静态成员函数
cout << d2.GetN() << endl;
cout << A::GetN() << endl;
return 0;
}
特性
1、一个类的静态成员是由该类创建的所有对象共享的,存放在静态区
2、静态成员变量必须在类外定义
3、静态成员函数不单独属于某一个对象,所有没有隐含的this指针,所以也就不能在静态成员函数访问任何非静态成员。
【问题】
1、静态成员函数可以调用非静态成员函数吗?【不可以】
2、非静态成员函数可以调用类的静态成员函数吗?【可以】
原因:调用普通成员函数需要隐含的this指针,像这样:d.Print(); 这个d对象就传给Print函数的隐含的this指针,而静态成员函数是不含this指针的。而非静态成员函数就 可以调用静态成员函数,静态成员函数是可以在类中任何位置调用的,是共享的。
友元
友元分为友元函数和友元类
友元函数
使用格式:友元函数的声明在类中,定义在类外。
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d); // 存在返回值,因为要使用连续输入和输出
friend istream& operator>>(istream& in, Date& d); // 输入值,要改变成员变量,所以不用const
public:
Date(int year = 1, int month = 1, int day = 2)
:_year(year)
, _month(month)
, _day(day)
{}
// 对于输入和输出运算符重载,定义为成员函数的话,可读性差
ostream& operator<<(ostream& out) // -> d << cout; d.operator<<(cout); 不符合标准使用习惯
{
out << _year << _month << _day << endl;
return out;
}
istream& operator>>(istream& in) // -> d << cin; d.operator>>(cin):
{
in >> _year >> _month >> _day;
return in;
}
void Print()
{
cout << _year << _month << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << ' ' << d._month << ' ' << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
cin >> year >> month >> day;
d._year = year;
d._month = month;
d._day = day;
return in;
}
int main()
{
Date d;
//d << cout;
//d.operator<<( cout);
cout << d; // 这才习惯于使用习惯
cin >> d;
d.Print();
}
友元函数可以直接访问类的私有成员变量,但是定义在类外,不属于任何类,所以需要在类中声明,加上friend关键字
【注意】
1、友元函数不能用const修饰
2、可以在类定义的任何地方声明,不受类访问限定符限制
3、一个友元函数可以是多个类的友元函数
友元类
友元就是把它当成朋友,可以访问我的私有成员。
友元关系是单向的,不能交换
内部类
简单得说,就是在一个类中嵌套另一个类