文章目录
- 1.类的访问限定符
- 2.封装
- 3.类对象的存储方式
- 4.为什么要进行内存对齐?结构体怎么对齐?
- 5.如何让结构体按照指定的对齐参数进行对齐
- 6.如何知道结构体中某个成员相对于结构体起始位置的偏移量
- 7.C++有哪几种构造函数
- 8.类的六个默认成员函数
- 9.构造函数
- 10.析构函数
- 11.拷贝构造函数
- 12.运算符重载
- 13.浅拷贝和深拷贝
1.类的访问限定符
-
访问限定符:public、protected、private
-
public修饰的成员在类外可以直接被访问
-
protected和private修饰的成员在类外不能直接被访问(此处protected和private类似)
-
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
-
class的默认访问权限为private,struct为public(因为struct要兼容C)
2.封装
- C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部用户使用。
- 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装可以看作是将数据和操作包装在一个黑盒子中,只暴露出必要的接口,而隐藏了内部的实现细节
- 封装是为了实现以下目标:数据隐藏、接口统一、降低耦合、代码重用
3.类对象的存储方式
-
假设对象中包含类的各个成员,这样就有缺陷:每个对象中只有成员变量是不同的,但是调用同一份函数,如果按照这种方式存储,当一个类创建多个对象时,每个对象都会保存一份代码,相同代码保存多次,浪费空间。于是我们就有了这种存储方式:只保存成员变量,成员函数存放在公共的代码段。
-
总之,一个类的大小,实际即是该类中“成员变量”之和,也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类
4.为什么要进行内存对齐?结构体怎么对齐?
-
内存对齐是指将结构体的成员在内存中排列为一系列连续的块,并且要求成员的地址是其自身大小的整数倍。这样做的目的是为了优化内存访问速度和处理器的数据访问效率。
-
内存对齐的原因和优势包括:
-
-
性能优化:处理器通常在较对齐的内存地址上执行数据访问操作更快。
-
移植性:内存对齐可以确保代码在不同平台上的行为一致性,避免因为数据对齐不一致而产生的错误或异常。
-
结构体大小优化:有些编译器会对结构体进行优化,通过进行内存对齐来减少空洞和填充,从而减小结构体的大小。
-
-
常见的对齐规则
-
以成员的自身大小为单位,确保每个成员的起始地址是其自身大小的整数倍
-
第一个成员在与结构体偏移量为0的地址处
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
-
结构体总大小:最大对齐数(所有变量类型最大者与默认对其参数取最小的)的整数倍
-
如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
-
例如,假设有以下结构体:
struct MyStruct {
char a;
int b;
double c;
};
按4字节对齐:
a - 1字节
(3字节填充)
b - 4字节
c - 8字节
5.如何让结构体按照指定的对齐参数进行对齐
- 在 Visual Studio C++ 编译器中,可以使用
#pragma pack(n)
指令来指定结构体的对齐方式,其中n
表示对齐的字节数。在指定的结构体之后的代码中,都会按照指定的对齐方式进行对齐,直到遇到#pragma pack()
恢复默认对齐方式。
#pragma pack(8) // 指定后续结构体按照 8 字节对齐
struct MyStruct {
char a;
int b;
double c;
};
#pragma pack() // 恢复默认对齐方式
6.如何知道结构体中某个成员相对于结构体起始位置的偏移量
-
可以使用
offsetof
宏来获取结构体中某个成员相对于结构体起始位置的偏移量。offsetof
宏定义在<cstddef>
头文件中(在 C 中定义在<stddef.h>
头文件中),它能够帮助我们在编译时获取结构体成员的偏移量。 -
offsetof
宏的使用方式如下:
#include <cstddef>
struct MyStruct {
int a;
double b;
char c;
};
int main() {
size_t offsetA = offsetof(MyStruct, a);
size_t offsetB = offsetof(MyStruct, b);
size_t offsetC = offsetof(MyStruct, c);
std::cout << "Offset of 'a': " << offsetA << std::endl;
std::cout << "Offset of 'b': " << offsetB << std::endl;
std::cout << "Offset of 'c': " << offsetC << std::endl;
cout << sizeof(MyStruct) << endl;
return 0;
}
输出结果:
Offset of 'a': 0
Offset of 'b': 8
Offset of 'c': 16
24
结构体的成员在内存中是按照定义的顺序存放的,偏移量是成员相对于结构体起始位置的字节偏移。
7.C++有哪几种构造函数
- 默认构造函数
- 初始化构造函数
- 拷贝构造函数
- 移动构造函数
- 委托构造函数
- 转换构造函数
8.类的六个默认成员函数
- 任何一个类在我们自己不写的情况下,都会自动生成下面六个默认成员函数(包括空类)
- 初始化和清理:
- 构造函数主要完成初始化工作
- 析构函数主要完成清理工作
- 拷贝复制
- 拷贝构造:用同类对象初始化创建对象
- 赋值运算符重载:把一个对象赋值给另一个对象
- 取地址重载
- 主要是普通对象和const对象取地址(这两个很少会自己实现)
9.构造函数
-
构造函数是特殊的成员函数,主要任务是初始化对象
-
构造函数特征如下:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们未显式定义编译器默认生成的构造函数,都可以认为是默认成员函数
-
关于编译器生成的默认成员函数的作用:在我们不实现构造函数的情况下,编译器会生成默认的构造函数,但是此时我们的成员变量依旧是随机值,那编译器生成的默认构造函数有什么作用呢?
C++把类型分成内置类型(基本类型)和自定义类型。观察下面代码,编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time{
public:
Time(){
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main(){
Date d;
return 0;
}
10.析构函数
-
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁 ,局部对象销毁工作是由编译器完成的。对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
-
析构函数特征:
-
析构函数名是在类名前加上字符~
-
无参数无返回值
-
一个类有且只有一个析构函数,若未显式定义,系统会自动生成默认的析构函数
-
对象生命周期结束时,C++编译系统自动调用析构函数
-
关于编译器自动生成的默认析构函数,对自定义类型成员调用它的析构函数
-
11.拷贝构造函数
- 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数特征:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷 贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
12.运算符重载
- C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
- 函数名字为:关键字operator后面接需要重载的运算符符号
- 函数原型:返回值类型 operator操作符(参数列表)
- 注意:
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参
.*
成员指针访问运算符、::
作用域运算符、sizeof
、?:
条件运算符、.
成员访问运算符 不能重载
13.浅拷贝和深拷贝
-
浅拷贝是简单的将一个对象的数据成员的值赋值到另一个对象中。浅拷贝只复制指针的值,而不复制指针指向的内容 ,因此,原对象会和拷贝对象会指向同一块内存地址。如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。浅拷贝适用于简单的数据类型,以及不涉及资源管理的类对象
-
深拷贝是在进行拷贝时,会为目标对象重新分配独立的内存,并复制源对象的内容到新分配的内存中,而不是简单的复制指针的值。这样两个对象完全独立,互不影响。深拷贝适用于包含指针成员的类对象,以及设计资源管理(例如动态内存分配、文件句柄等)的情况。
示例代码:
class String{
public:
String(const char* str = "\0")
//字符串常量要用常指针,要加const
{
m_data = (char*)malloc(strlen(str) + 1);//深拷贝
//开辟空间要考虑字符串结束标志\0,所以要加一
strcpy(m_data, str);
cout << m_data << " : " << &m_data << endl;
}
String(const String &s)
{
m_data = (char*)malloc(strlen(s.m_data) + 1);
strcpy(m_data, s.m_data);
cout << m_data << " : " << &m_data << endl;
}
~String()
{
free(m_data);
}
private:
char *m_data = nullptr;
};
int main()
{
String s("Hello");
String s1 = s;//拷贝构造
return 0;
}
最后发现s和s1是独立的两个对象:
Hello : 004FFE0C
Hello : 004FFE00