1 C++ 中 sizeof 关键字
1.1 sizeof 关键字
sizeof计算大小的小技巧
· 指针的大小永远是固定的,取决于处理器位数,32位就是 4 字节,64位就是 8 字节
· 数组作为函数参数时会退化为指针,大小要按指针的计算。
· struct 结构体要考虑字节对齐。
· 字符串数组要算上末尾的 '\0'
// 前提: 64 位电脑上
char str[] = "Hello World" ;
char *p = str;
double *dp;
int n = 10;
sizeof(str )=___12_____ // 11个字符 + 末尾'\0',总结第四点
sizeof ( p ) = ___8 ___ // 64 位电脑,指针 8 个字节
sizeof ( n ) = ___4______ // int 一般 4 个字节
void Func (char str[10])
{
sizeof( str ) = _8__ // 数组做参数退化为 char类型指针,即 8 个字节,总结第2点
}
void *vp = malloc( 100 );
sizeof ( vp )=__8____ // vp 是一个 void 类型指针,还是 8 个字节
struct AlignedStruct {
char a; // 本来1字节,padding 3 字节
int b; // 4 字节
short c; // 本来 short 2字节,但是整体需要按照 4 字节对齐(成员对齐边界最大的是int 4) ,
//所以需要padding 2,总共: 4 + 4 + 4
};
sizeof(AlignedStruct) = ___12__
struct MyStruct {
int a;
double b;
char c;
};//24
//int a 位于 0~3 字节,占 4 个字节。
//double b 需要对齐到 8 字节边界,所以在 4~7 字节填充 4 个字节的 padding,并从字节 8 开始放置 //double b, 占据 8 至 15 字节。
//char c 位于 16 字节,占 1 个字节。
//由于 double 需要对齐到 8 字节边界,结构体的整体大小也需要遵循此规则,所以 MyStruct 的总大小在 //17 至 24 字节之间又增加了 7 个字节的 padding,使其总大小达到 24 字节。
int arr[10];//40
struct EmptyStruct {};//1
1.2 sizeof 和 strlen
strlen
strlen 是一个 C 标准库中的函数,用于计算 C 风格字符串(以空字符 '\0' 结尾的字符数组)的长度,即不包括结尾的空字符的字符个数。
#include <iostream>
#include <cstring>
int main() {
char str[] = "Hello, world!";
std::cout << "Length of str: " << strlen(str) << std::endl; // 输出字符串 str 的长度
}
strlen 源代码如下:
size_t strlen(const char *str) {
size_t length = 0;
while (*str++)
++length;
return length;
}
#sizeof
sizeof 是一个 C++ 编译期间计算的操作符,用于计算数据类型或对象所占用的字节数。
#include <iostream>
int main() {
int a = 42;
std::cout << "Size of int: " << sizeof(int) << std::endl; // 输出 int 类型的大小
std::cout << "Size of a: " << sizeof(a) << std::endl; // 输出变量 a 的大小
std::cout << "Size of double: " << sizeof(double) << std::endl; // 输出 double 类型的大小
}
2. 数据类型
2.1 整型
C++ 整型数据长度标准
short ⾄少 16 位;int ⾄少与 short ⼀样长;long ⾄少 32 位,且⾄少与 int ⼀样长;long long ⾄少 64 位,且⾄少与 long ⼀样长;在使⽤8位字节的系统中,1 byte = 8 bit;很多系统都使⽤最⼩长度,short 为 16 位即 2 个字节,long 为 32 位即 4 个字节,long long 为 64 位即 8 个字节,
int 的长度较为灵活,⼀般认为 int 的长度为 4 个字节,与 long 等长。
头⽂件climits定义了符号常量:例如:INT_MAX 表⽰ int 的最⼤值,INT_MIN 表⽰ int 的最⼩值。
2.2 ⽆符号类型
即为不存储负数值的整型,可以增⼤变量能够存储的最⼤值,数据长度不变。
3. 指针
3.1 C/C++中数组做参数退化为指针
C++ 面试中还有一个比较常见的考题,就是会将一个数组做参数,然后在函数内部用 sizeof 去判断这个数组参数的大小,如下:
int func(char array[]) {
printf("sizeof=%d\n", sizeof(array));//8
printf("strlen=%d\n", strlen(array));//11
}
int main() {
char array[] = "Hello World";
printf("sizeof=%d\n", sizeof(array));//12
printf("strlen=%d\n", strlen(array));//11
func(array);
}
3.2 指针和引用的区别
-
指针是一个新的变量,指向另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;而引用是一个别名,对引用的操作就是对变量的本身进行操作
-
指针可以有多级,引用只有一级
-
传参的时候,使用指针的话需要解引用才能对参数进行修改,而使用引用可以直接对参数进行修改
-
指针的大小一般是4个字节,引用的大小取决于被引用对象的大小
-
指针可以为空,引用不可以。
数组做参数退化为指针
数组退化:在 C++ 中,数组在作为函数参数时会退化为指向其首元素的指针。
退化的原因是因为数组作为函数参数时,实际传递的是指向数组首元素的指针,不可能逐个拷贝整个数组然后在栈上传递,所以编译器只知道参数是一个指针,而不知道它的长度信息。
但是,当数组直接作为 sizeof 的参数时,它不会退化,因为 sizeof 是编译器在编译期间计算的结果,这个时候编译器是有信息知道数组的大小。
为了在函数中获取数组的长度,需要将数组的长度作为另一个参数传递给函数,或者使用模板实现。
4 C++ 中 const 关键字
4.1修饰变量
当 const 修饰变量时,该变量将被视为只读变量,即不能被修改。
对于确定不会被修改的变量,应该加上 const,这样可以保证变量的值不会被无意中修改,也可以使编译器在代码优化时更加智能。
const int a = 10;
a = 20; // 编译错误,a 是只读变量,不能被修改
在 C++ 中,将 const 类型的指针强制转换为非const 类型的指针被称为类型强制转换(Type Casting),这种行为称为 const_cast。
4.2修饰函数参数
当 const 修饰函数参数时,表示函数内部不会修改该参数的值。这样做可以使代码更加安全,避免在函数内部无意中修改传入的参数值。
尤其是 引用 作为参数时,如果确定不会修改引用,那么一定要使用 const 引用。
例如:
void func(const int a) {
// 编译错误,不能修改 a 的值
a = 10;
}
4.3 修饰指针或引用
在 C/C++ 中,const 关键字可以用来修饰指针,用于声明指针本身为只读变量或者指向只读变量的指针。
根据 const 关键字的位置和类型,可以将 const 指针分为以下三种情况:
指向只读变量的指针
这种情况下,const 关键字修饰的是指针所指向的变量,而不是指针本身。
因此,指针本身可以被修改(意思是指针可以指向新的变量),但是不能通过指针修改所指向的变量。
const int* p; // 声明一个指向只读变量的指针,可以指向 int 类型的只读变量
int a = 10;
const int b = 20;
p = &a; // 合法,指针可以指向普通变量
p = &b; // 合法,指针可以指向只读变量
*p = 30; // 非法,无法通过指针修改只读变量的值
在上面的例子中,我们使用 const int* 声明了一个指向只读变量的指针 p。
我们可以将指针指向普通变量或者只读变量,但是无法通过指针修改只读变量的值。
只读指针
这种情况下,const 关键字修饰的是指针本身,使得指针本身成为只读变量。
因此,指针本身不能被修改(即指针一旦初始化就不能指向其它变量),但是可以通过指针修改所指向的变量。
int a = 10;
int b = 20;
int* const p = &a; // 声明一个只读指针,指向 a
*p = 30; // 合法,可以通过指针修改 a 的值
p = &b; // 非法,无法修改只读指针的值
在上面的例子中,我们使用 int* const 声明了一个只读指针 p,指向变量 a。我们可以通过指针修改 a 的值,但是无法修改指针的值。
只读指针指向只读变量
这种情况下,const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。
因此,指针本身不能被修改,也不能通过指针修改所指向的变量。
const int a = 10;
const int* const p = &a; // 声明一个只读指针,指向只读变量 a
*p = 20; // 非法,无法通过指针修改只读变量的值
p = nullptr; // 非法,无法修改只读指针的值
常量引用
常量引用是指引用一个只读变量的引用,因此不能通过常量引用修改变量的值。
const int a = 10;
const int& b = a; // 声明一个常量引用,引用常量 a
b = 20; // 非法,无法通过常量引用修改常量 a 的值
修饰成员函数
当 const 修饰成员函数时,表示该函数不会修改对象的状态(就是不会修改成员变量)。
这样有个好处是,const 的对象就可以调用这些成员方法了,因为 const 对象不允许调用非 const 的成员方法。
也很好理解,既然对象是 const 的,那我怎么保证调用完这个成员方法,你不会修改我的对象成员变量呢?那就只能你自己把方法声明未 const 的呢~
例如:
class A {
public:
int func() const {
// 编译错误,不能修改成员变量的值
m_value = 10;
return m_value;
}
private:
int m_value;
};
这里还要注意,const 的成员函数不能调用非 const 的成员函数,原因在于 const 的成员函数保证了不修改对象状态,但是如果调用了非 const 成员函数,那么这个保证可能会被破坏。
总之,const 关键字的作用是为了保证变量的安全性和代码可读性。
我对const的理解就是const在什么变量的左边,变量就不能被修改。
5 C++ 中 static 关键字
5.1 static 修饰全局变量
static 修饰全局变量可以将变量的作用域限定在当前文件中,使得其他文件无法访问该变量。 同时,static 修饰的全局变量在程序启动时被初始化(可以简单理解为在执行 main 函数之前,会执行一个全局的初始化函数,在那里会执行全局变量的初始化),生命周期和程序一样长。
// a.cpp 文件
static int a = 10; // static 修饰全局变量
int main() {
a++; // 合法,可以在当前文件中访问 a
return 0;
}
// b.cpp 文件
extern int a; // 声明 a
void foo() {
a++; // 非法,会报链接错误,其他文件无法访问 a
}
5.2static 修饰局部变量
static 修饰局部变量可以使得变量在函数调用结束后不会被销毁,而是一直存在于内存中,下次调用该函数时可以继续使用。
同时,由于 static 修饰的局部变量的作用域仅限于函数内部,所以其他函数无法访问该变量。
void foo() {
static int count = 0; // static 修饰局部变量
count++;
cout << count << endl;
}
int main() {
foo(); // 输出 1
foo(); // 输出 2
foo(); // 输出 3
return 0;
}
5.3 static 修饰函数
static 修饰函数可以将函数的作用域限定在当前文件中,使得其他文件无法访问该函数。
同时,由于 static 修饰的函数只能在当前文件中被调用,因此可以避免命名冲突和代码重复定义。
// a.cpp 文件
static void foo() { // static 修饰函数
cout << "Hello, world!" << endl;
}
int main() {
foo(); // 合法,可以在当前文件中调用 foo 函数
return 0;
}
// b.cpp 文件
extern void foo(); // 声明 foo
void bar() {
foo(); // 非法,会报链接错误,找不到 foo 函数,其他文件无法调用 foo 函数
}
5.4static 修饰类成员变量和函数
static 修饰类成员变量和函数可以使得它们在所有类对象中共享,且不需要创建对象就可以直接访问。
class MyClass {
public:
static int count; // static 修饰类成员变量
static void foo() { // static 修饰类成员函数
cout << count << endl;
}
};
// 访问:
MyClass::count;
MyClass::foo();
6 字节对齐
在C/C++中,字节对齐是内存分配的一种策略。
当分配内存时,编译器会自动调整数据结构的内存布局,使得数据成员的起始地址与其自然对齐边界(一般为自己大小的倍数)相匹配。
字节对齐有助于提高内存访问速度,因为许多处理器都优化了对齐数据的访问。但是,这可能会导致内存中的一些空间浪费。
6.1字节对齐规则
以下是字节对齐的一些基本规则:
#1. 自然对齐边界
对于基本数据类型,其自然对齐边界通常为其大小。
例如,char 类型的自然对齐边界为 1 字节,short 为 2 字节,int 和 float 为 4 字节,double 和 64 位指针为 8 字节。具体数值可能因编译器和平台而异。
#2. 结构体对齐
结构体内部的每个成员都根据其自然对齐边界进行对齐。
也就是可能在成员之间插入填充字节。
结构体本身的总大小也会根据其最大对齐边界的成员进行对齐(比如结构体成员包含的最长类型为int类型,那么整个结构体要按照4的倍数对齐),以便在数组中正确对齐。
#3. 联合体对齐
联合体的对齐边界取决于其最大对齐边界的成员。联合体的大小等于其最大大小的成员,因为联合体的所有成员共享相同的内存空间。
#4. 编译器指令
可以使用编译器指令(如 #pragma pack)更改默认的对齐规则。这个命令是全局生效的。这可以用于减小数据结构的大小,但可能会降低访问性能。
#5. 对齐属性
在 C++11 及更高版本中,可以使用 alignas 关键字为数据结构或变量指定对齐要求。这个命令是对某个类型或者对象生效的。例如,alignas(16) int x; 将确保 x 的地址是 16 的倍数。
#6. 动态内存分配
大多数内存分配函数(如 malloc 和 new)会自动分配足够对齐的内存,以满足任何数据类型的对齐要求。
#include <iostream>
#pragma pack(push, 1) // 设置字节对齐为 1 字节,取消自动对齐
struct UnalignedStruct {
char a;
int b;
short c;
};
#pragma pack(pop) // 恢复默认的字节对齐设置
struct AlignedStruct {
char a; // 本来1字节,padding 3 字节
int b; // 4 字节
short c; // 本来 short 2字节,但是整体需要按照 4 字节对齐(成员对齐边界最大的是int 4)
// 所以需要padding 2
// 总共: 4 + 4 + 4
};
struct MyStruct {
double a; // 8 个字节
char b; // 本来占一个字节,但是接下来的 int 需要起始地址为4的倍数
//所以这里也会加3字节的padding
int c; // 4 个字节
// 总共: 8 + 4 + 4 = 16
};
struct MyStruct1 {
char b; // 本来1个字节 + 7个字节padding
double a; // 8 个字节
int c; // 本来 4 个字节,但是整体要按 8 字节对齐,所以 4个字节padding
// 总共: 8 + 8 + 8 = 24
};
int main() {
std::cout << "Size of unaligned struct: " << sizeof(UnalignedStruct) << std::endl;
// 输出:7
std::cout << "Size of aligned struct: " << sizeof(AlignedStruct) << std::endl;
// 输出:12,取决于编译器和平台
std::cout << "Size of aligned struct: " << sizeof(MyStruct) << std::endl;
// 输出:16,取决于编译器和平台
std::cout << "Size of aligned struct: " << sizeof(MyStruct1) << std::endl;
// 输出:24,取决于编译器和平台
return 0;
}
7 C/C++ 字节序
8 C++ 中 extern 作用
一般而言,C++全局变量的作用范围仅限于当前的文件,但同时C++也支持分离式编译,允许将程序分割为若干个文件被独立编译。
于是就需要在文件间共享变量数据,这里extern就发挥了作用。
extern 用于指示变量或函数的定义在另一个源文件中,并在当前源文件中声明。 说明该符号具有外部链接(external linkage)属性。
也就是告诉编译器: 这个符号在别处定义了,你先编译,到时候链接器会去别的地方找这个符号定义的地址。
声明变量或函数的存在,但不进行定义,让编译器在链接时在其他源文件中查找定义。
这使得不同的源文件可以共享相同的变量或函数。
当链接器在一个全局变量声明前看到 extern 关键字,它会尝试在其他文件中寻找这个变量的定义。
这里强调全局且非常量的原因是,全局非常量的变量默认是外部链接的。
//fileA.cpp
int i = 1; //声明并定义全局变量i
//fileB.cpp
extern int i; //声明i,链接全局变量
//fileC.cpp
extern int i = 2; //错误,多重定义
int i; //错误,这是一个定义,导致多重定义
main()
{
extern int i; //正确
int i = 5; //正确,新的局部变量i;
}
9 堆和栈
9.1 堆和栈有什么区别
-
从定义上:堆是由new和malloc开辟的一块内存,由程序员手动管理,栈是编译器自动管理的内存,存放函数的参数和局部变量。
-
堆空间因为会有频繁的分配释放操作,会产生内存碎片
-
堆的生长空间向上,地址越来越大,栈的生长空间向下,地址越来越小。
9.2 堆和栈谁快一点?
栈快一点。因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
10 malloc/free/new/delete
10.1 new和delete是如何实现的,new 与 malloc的异同处
在new一个对象的时候,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数。delete会调用对象的析构函数,然后调用free回收内存。
new与malloc都会分配空间,但是new还会调用对象的构造函数进行初始化,malloc需要给定空间大小,而new只需要对象名。
10.2 既然有了malloc/free,C++中为什么还需要new/delete呢?
-
malloc/free和new/delete都是用来申请内存和回收内存的。
-
在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free。
10.3 delete和delete[]的区别
-
delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数
-
用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
11 C++强制类型转换
四种强制类型转换操作符分别为:static_cast、dynamic_cast、const_cast、reinterpret_cast
-
1)static_cast : 用于各种隐式转换。具体的说,就是用户各种基本数据类型之间的转换,比如把int换成char,float换成int等。以及派生类(子类)的指针转换成基类(父类)指针的转换。
特性与要点:
-
它没有运行时类型检查,所以是有安全隐患的。
-
在派生类指针转换到基类指针时,是没有任何问题的,在基类指针转换到派生类指针的时候,会有安全问题。
-
static_cast不能转换const,volatile等属性
-
-
2)dynamic_cast: 用于动态类型转换。具体的说,就是在基类指针到派生类指针,或者派生类到基类指针的转换。 dynamic_cast能够提供运行时类型检查,只用于含有虚函数的类。 dynamic_cast如果不能转换返回NULL。
-
3)const_cast: 用于去除const常量属性,使其可以修改 ,也就是说,原本定义为const的变量在定义后就不能进行修改的,但是使用const_cast操作之后,可以通过这个指针或变量进行修改; 另外还有volatile属性的转换。
-
4)reinterpret_cast 几乎什么都可以转,用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换等。但是不够安全。
12 运算符重载
参考:运算符重载