C++ 重点的关键字
以下内容整理自 ”编程指北“ 大佬
链接: https://csguide.cn/cpp/basics/const.html#_4-%E4%BF%AE%E9%A5%B0%E6%8C%87%E9%92%88%E6%88%96%E5%BC%95%E7%94%A8
sizeof
sizeof是C语言中的运算符,用来计算一个类型/结构所占用的字节数。
sizeof的运算技巧:
- 使用sizeof计算指针时,我们需要知道指针的大小都是固定的,且指针的字节大小是处理决定的,32位处理器就是4字节,64位是8字节。
- 数组作为函数参数被sizeof计算时,这个数组的字节大小和指针一样,这是因为数组退化指针的原理。
- struct结构体被sizeof进行计算时,需要注意内存对齐的原则,以及遵守最大对齐数倍数的原则。
- 字符串如果被sizeof计算时,需要算上末尾的\0,这是因为C语言风格的字符串在末尾都会默认的加上一个\0作为结束标识符。
数组退化指针:
- 数组退化指针的原因是,当数组作为参数被函数引用时,传递的并不是数组本身,而是传递了一个指针,准确的说传递的是数组首个元素的地址,也就是指向数组首个元素地址的指针,所以在传参的过程中,编译器识别到的是指针,而不是整个数组。
- 但是如果传参时使用的是数组的引用,那么传递的才是真正的数组的大小, 而不是会被编译器识别为指针。
内存对齐的原则:
1. 结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩(变量的字节大小)的较⼩值。
VS中默认的值为8
Linux中没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
sizeof和数组
//关于sizeof 和数组
int main()
{
int arr[] = { 1,2,3,4,5 };
cout << "直接sizeof(数组名):" << sizeof(arr) << endl;
//20,sizeof(数组名)是把整个数组的字节数全部和在了一起
cout << "直接sizeof(数组名+数字):" << sizeof(arr+0) << endl;
//4 sizeof(数组名+数字)其实是表示了数组内部第某个元素的大小 ===> sizeof(数组名[数字])
cout << "直接sizeof(*数组名):" << sizeof(*arr) << endl;
//4 sizeof(*数组名) 其实使用了*就表示当前传递的是一个指针,哪怕*后面的是一个数组名
//这个和数组退化为指针有关系,我们*数组名 的本质是传递一个地址,但是并不可能传递整个数组上面所有元素的地址
//于是传递的就是数组上首个元素的地址,这里首个元素的地址 原因是因为数组上面的空间是连续的,所以只要不越界即可
//所以sizeof(*数组名) 本质上是在计算数组上首个元素地址的字节大小
cout << "直接sizeof(数组名[数字]):" << sizeof(arr[2]) << endl;
//4 sizeof(数组名[数字])表示的其实是数组上某个元素的大小,
//在这里sizeof(arr[2])表示的就是数组下标为2的元素的字节大小
cout << "直接sizeof(&数组名):" << sizeof(&arr) << endl;
// 4 &数组名 其实本质就是整个数组的地址,需要了解一下,数组的地址,并不等于数组内部所有元素的地址
//所以,数组的地址,就是地址,也就是一个指针,所以对于一个指针来说,sizeof(&数组名) = sizeof(指针)
cout << "直接sizeof(* &数组名):" << sizeof(*&arr) << endl;
//20 sizeof(* &数组名)表示的是计算数组内部所有元素的地址的 字节大小 之和
cout << "直接sizeof(&数组名+1):" << sizeof(&arr+1) << endl;
//4 &数组名 表示的是整个数组的地址,所以&数组名 是一个地址, 地址+1 也就是加上一个当前地址大小后的地址
//可以理解为,当前的地址是a a+1 后 a+1 = 2a 所以&数组名+1 本质上就是一个地址,一个跨越了当前地址大小的地址
cout << "直接sizeof(&数组名[数字]):" << sizeof(&arr[0]) << endl;
//4 数组名[数字] 表示的是一个数组内部的一个元素,而&数组名[数字] 就可以表示 这个元素的地址
//所以 sizeof(&数组名[数字]) 表示的是一个元素的地址 的字节大小!
cout << "直接sizeof(&数组名[数字]+1):" << sizeof(&arr[0]+1) << endl;
// 4 &数组名[数字] 本质是一个地址,而地址+1 的本质 也是一个地址,
//所以,这里的sizeof(&数组名[数字]+1)是一个地址的大小
return 0;
}
- 总结,数组和sizeof 需要考虑 数组名是处在一种什么样子的情况
- 如果只是单单一个数组名,也就是 sizeof(数组名) ,那么这个数组名表示的就是数组所有的元素
- 如果又数组名加上其他的东西,我们就要进行思考,思考加上其他字符组合的数组名扮演的是首元素地址,还是其他
- &数组名 表示的是数组的地址
strlen
strlen是一个从标准库的函数,用于计算C语言风格的的字符串,也就是会在字符串末尾默认加上\0作为结束标识符的字符串。
//字符数组 和 strlen
int main()
{
char arr[] = { 'a','b','c','d','e' };
cout<<"sizeof(数组名):" << sizeof(arr) << endl;
//cout <<"strlen(数组名):" << strlen(arr) << endl;
cout << "strlen(字符串):" << strlen("abcde") << endl;
cout << "sizeof(字符串):" << sizeof("abcde") << endl;
//sizeof(数组名) 5 strlen(数组名) 随机数! strlen(字符串) 5 sizof(字符串) 6
//
//
// 关于strlen(数组名) 随机数的原因
// 因为strlen是用于计算C语言风格的字符数组,
//也就是说strlen在进行字符数组运算时,或者字符运算时需要一个\0进行刹车,否则strlen会产生一个随机数
//而产生的随机数表示的就是strlen在该字符数组的基础上进行遍历,直到它遇见了\0才停下
//
//sizeof(字符串) 6 但是sizeof(数组名) 5 且数组的内部的元素个数和元素和字符串的元素个数和字符是一样的
//这其实也是和C语言风格的字符串有关
//C语言风格的字符串的内部是自带一个停止符,也就是\0的所以在C语言中,
//每一个字符串的大小都等于所有的 字符的字节大小之和加上一个\0的字节大小
char arr1[] = { 'a','b','c','d','e' ,'\0' };
cout << "strlen(数组名):" << strlen(arr1) << endl;//5
cout << "strlen(数组名+数字):" << strlen(arr1 + 0) << endl;//5
cout << "strlen(数组名+数字):" << strlen(arr1+3) << endl;//2
cout << "strlen(数组名+数字):" << strlen(arr1 + 2) << endl;//3
//通过strlen(数组名+数字)的三次测试,和三次测试的结果,我们可以得到
//strlen(数组名+数字)的含义:
//因为数组名可以代表首元素地址,所以首元素地址+数字的含义就是从首元素地址往后挪动了几次,所抵达的地址位置
//然后从该地址位置开始进行strlen的遍历计算
//所以strlen(arr1+0)表示在下标0的位置进行strlen的字符数组长度计算
//strlen(arr1+3)表示在下标3的位置进行strlen的字符数组长度计算
//strlen(arr1+2)表示在下标2的位置进行strlen的字符数组长度计算
//strlen(arr[1])、strlen(&arr1)、 strlen(*arr1)是不能被实现的
//strlen的一个本质就是,它的内部隐藏着一个指向字符数组的首个元素地址的指针
//而这个指针只能通过读取到\0才能停下,若字符数组中并没有\0存在,那么它会进行越界寻找\0直到找到才停下
return 0;
}
sizof和strlen的区别:
C语言的字符串都是会默认带上一个\0作为标识符,所以sizeof在进行计算字符串大小时,会加上一个\0的字节大小,而strlen在计算一个字符串时,会默认的去掉这个\0,从而计算这个字符串真实的长度
const
在C++/C语言中,const用来表示的是常量,const可以用于修饰变量、函数、指针等等。
1、修饰变量
const int a = 10;
a = 20; // 编译错误,a 是只读变量,不能被修改
当const修饰一个变量时,该变量会被const变为一个常量,而在C++/C语言中,常量通常是不允许被改变的,所以该变量是不会被改变,也不能被赋值。
同时当一个变量被const修饰后,这个变量会被称作只读变量,表示该变量只能被读取,不能被修改。
但是,如果想要对该变量进行数值的修改也就是改变这个变量的数值,可以使用const_cast进行强制类型转换。
const int a = 10;
const int* p = &a;
int* q = const_cast<int*>(p);
*q = 20; // 通过指针间接修改 const 变量的值
std::cout << "a = " << a << std::endl; // 输出 a 的值,结果为 10
如上代码所示,使用了一个const 修饰的int类型指针获取了被const修饰的变量地址,在通过const_cast进行指针类型的强转,从而去掉const 。
2、修饰函数参数,表示函数不会修改参数
当const修饰函数参数时,表示了该函数内部并不能修改这个参数的数值,这样做会使代码更加的安全,同时避免在函数内部无意识的修改传入的函数值,或者避免传入的函数值被编译器优化。
void func(const int a) {
// 编译错误,不能修改 a 的值
a = 10;
}
3、修饰函数返回值
当const修饰函数返回值时,表示该函数的返回值只能读取,并不能修改,这样做可以让函数的返回值更加的安全,避免被误修改。
const int func() {
int a = 10;
return a;
}
int main() {
const int b = func(); // b 的值为 10,不能被修改
b = 20; // 编译错误,b 是只读变量,不能被修改
return 0;
}
4、修饰成员函数
当const修饰成员函数时,表示该函数并不会修改对象的状态(也就是并不会对成员变量进行修改)
这样的好处就是,被const修饰的对象就可以调用这些成员方法/成员变量,因为const对象是不能调用非const的成员变量和方法
也就是只有const可以调用const,非const调用不了const
class A {
public:
int func() const {
// 编译错误,不能修改成员变量的值
m_value = 10;
return m_value;
}
private:
int m_value;
};
5、修饰指针或引用
在 C/C++ 中,const 关键字可以用来修饰指针,用于声明指针本身为只读变量或者指向只读变量的指针。
根据 const 关键字的位置和类型,可以将 const 指针分为以下三种情况:
5.1、指向只读变量的指针 (const在*的左边方向)
这种情况下,const 关键字修饰的是指针所指向的变量,而不是指针本身。
因此,指针本身可以被修改(意思是指针可以指向新的变量),但是不能通过指针修改所指向的变量。
const int* p; // 声明一个指向只读变量的指针,可以指向 int 类型的只读变量
int a = 10;
const int b = 20;
p = &a; // 合法,指针可以指向普通变量
p = &b; // 合法,指针可以指向只读变量
*p = 30; // 非法,无法通过指针修改只读变量的值
也就是可以修改指针的指向,但是不能修改指针指向的变量 的数值 !
5.2 、只读指针 (const 在*的右边方向)
这种情况下,const 关键字修饰的是指针本身,使得指针本身成为只读变量。
因此,指针本身不能被修改(即指针一旦初始化就不能指向其它变量),但是可以通过指针修改所指向的变量。
int a = 10;
int b = 20;
int* const p = &a; // 声明一个只读指针,指向 a
*p = 30; // 合法,可以通过指针修改 a 的值
p = &b; // 非法,无法修改只读指针的值
也就是可以改变指针指向的变量 的数值,但是不能对指针的指向进行修改,也就是指针被修饰后,只能指向这个地址,不能指向其他地址!
5.3 、只读指针指向只读变量 (const在*的左右两个方向都有)
这种情况下,const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。
因此,指针本身不能被修改,也不能通过指针修改所指向的变量。
const int a = 10;
const int* const p = &a; // 声明一个只读指针,指向只读变量 a
*p = 20; // 非法,无法通过指针修改只读变量的值
p = nullptr; // 非法,无法修改只读指针的值
也就是既不能修改指针指向的地址,也不能修改指针指向的变量 的数值!
5.4 、 常量引用
常量引用是指引用一个只读变量的引用,因此不能通过常量引用修改变量的值。
const int a = 10;
const int& b = a; // 声明一个常量引用,引用常量 a
b = 20; // 非法,无法通过常量引用修改常量 a 的值
static
常用于变量、类和函数中。
1、修饰全局变量
- static如果修饰全局变量,那么可将被修饰的变量限定在当前所处在的文件内,使得其他的文件无法通过链接或者头文件队该变量进行访问。
- 同时被static修饰的变量在程序启动时会进行自动的初始化,可以理解为,该变量在main函数之前被初始化了,且该变量的生命周期和程序一样长。
// a.cpp 文件
static int a = 10; // static 修饰全局变量
int main() {
a++; // 合法,可以在当前文件中访问 a
return 0;
}
// b.cpp 文件
extern int a; // 声明 a
//这里使用了extern 进行链接,但是会链接出错,因为全局变量a被static修饰了
void foo() {
a++; // 非法,会报链接错误,其他文件无法访问 a
}
2、修饰局部变量
- static修饰的局部变量,会使得该局部变量在函数调用结束后依旧存在,并不会被销毁,一直存在内存当中,等待下次使用(下次使用的数值是继承上次调用的)。
- 同时该局部变量被static修饰后,这个变量的作用域就只能局限于这个函数或者是类的范围之内了,其他函数或者类是无法进行调用的。
void foo() {
static int count = 0; // static 修饰局部变量
count++;
cout << count << endl;
}
int main() {
foo(); // 输出 1
foo(); // 输出 2
foo(); // 输出 3
return 0;
}
3、修饰函数
- 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 函数
}
4、修饰类成员变量和函数
- static所修饰的类成员和类内部的函数,可以让这些被修饰的成员和函数可以在所有的对象中共享使用,且不需要创建对应的对象就可以直接访问。
class MyClass {
public:
static int count; // static 修饰类成员变量
static void foo() { // static 修饰类成员函数
cout << count << endl;
}
};
// 访问:
MyClass::count;
MyClass::foo();
volatile
- volatile是一个用于修饰变量的关键字,主要的作用就是禁止编译器对 它所修饰的变量进行优化,以确保变量的数值每次都是从内存中进行读取的,而不是从寄存器或者缓存器中读取的。
- 简单来说,就是防止编译器优化,来确保结果能在预期范围之内。
define
- define 宏定义,在C++/C语言中宏定义的本质就是一种预处理指令的一种,是一种单纯的文本替换,它是在编译期间将宏进行展开,并进行替换功能,把代码内容替换为宏定义的内容。
- 同时因为宏定义只是单纯的文本替换,所以不会涉及定义内容的类型检查。
- 同时宏定义并不会受限于作用域,只要在宏定义之后的代码内容都可以使用和调用宏定义。
inline
- inline是定义函数为内联函数的关键字,内联函数的定义和普通函数类似,只需要在函数声明上加上inline即可。
- 对于内联函数,它的优点是类型安全、可调式、可优化,但是也存在一些问题。
- 由于被inline所修饰的函数体可能会被调用多次,也就是被赋值多次,这会导致代码段空间会被一段相同的代码所占用,最后演变成代码膨胀。
inline int square(int x) {
return x * x;
}
int main() {
int a = 5;
double b = 5.5;
// 下面这行代码将导致编译错误,因为类型不匹配
double result = square(b);
}
typedef
typedef是C++/C语言的一种定义别名的方法,用于将现有的名字改变成新的建议的名字,以此解决代码的可阅读性。
同时typedef在编译阶段会进行非常严格的类型检查,且typedef是遵守C++作用域的规则,会受到命名空间、类、函数等结构的作用于限制。
typedef通常用在复杂的别名或者是用在模板上。
//类型复杂的别名
typedef std::map<std::string, std::vector<int>> StringToIntVectorMap;
// 使用 typedef 定义模板类型别名
template <typename T>
struct MyContainer {
typedef std::vector<T> Type;
};
explicit
explicit的作用是防止不必要的隐式类型转化,以此提高代码的可读性和安全性。
隐式类型转化:
就是当你只有一个类型T1,但是当前表达式需要类型为T2的值,如果这时候T1自动转换为了T2,那么这就是隐式类型转换。
接下来通过一个例子来详细解释 explicit 的作用。
有一个类 MyInt,表示一个整数,并且有一个构造函数可以将 int 类型的参数转换为 MyInt 类型:
class MyInt {
public:
MyInt(int n) : num(n) {}
private:
int num;
};
我们可以使用下面的代码来创建一个 MyInt
对象:
MyInt a = 10; // 注意,这段代码有两个步骤:
// 1. int 类型的 10 先隐式类型转换为 MyInt 的一个临时对象
// 2. 隐式类型转换后的临时对象再通过复制构造函数生成 a
在一些情况下,上面这种隐式转换可能会导致问题。 例如,考虑下面的函数:
void f(MyInt n) {
// do something
}
如果我们调用这个函数,并传递一个 int
类型的值作为参数,如下所示:
f(10);
这也会编译通过,因为编译器会将 int 类型的值隐式转换为 MyInt 类型的对象。
但或许,有些情况下,我们并不期望 f 函数可以接受一个 int 类型的参数,这是预期外的,可能会导致错误的结果。
那么如果希望只接受 MyInt 类型的参数,就可以将构造函数声明加上 explicit:
class MyInt {
public:
explicit MyInt(int n) : num(n) {}
private:
int num;
};
这样,上面的调用语句将会导致编译错误,因为不能使用隐式转换将 int 类型的值转换为 MyInt 类型。
必须使用显式转换,如下所示:
f(MyInt(10));
所以大家日常可以使用 explicit 关键字可以防止不必要的隐式转换,提高代码的可读性和安全性。
尤其是构造函数参数只有一种类型的,强烈建议加上 explicit,目前我们部门的代码合并流水线就会按这个规则扫描。
extern
- extern的主要作用就是进行链接声明,因为C++是支持分离式编程的,允许将程序分割成若干个文件被独立编译,所以这就需要使得文件之间的数据进行共享。
- 而extern的作用就是将被修饰的变量或者函数的定义,出现在另一个源文件中,并在该文件中进行声明,进行链接,使得被修饰的变量或者函数可以在另一个文件中进行使用。
1、声明变量但不定义
声明变量或函数的存在,但不进行定义,让编译器在链接时在其他源文件中查找定义。
这使得不同的源文件可以共享相同的变量或函数。
当链接器在一个全局变量声明前看到 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;
}
2、 常量全局变量的外部链接
全局常量默认是内部链接的,所以想要在文件间传递全局常量量需要在定义时指明extern,如下所示:
//fileA.cpp
extern const int i = 1; //定义
//fileB.cpp //声明
extern const int i;
而下面这种用法则会报链接错误,找不到 i 的定义:
//fileA.cpp
const int i = 1; //定义 (不用 extern 修饰)
//fileB.cpp //声明
extern const int i;
3、编译和链接的过程
编译链接过程中,extern
的作用如下:
- 在编译期,
extern
用于告诉编译器某个变量或函数的定义在其他源文件中,编译器会为它生成一个符号表项,并在当前源文件中建立一个对该符号的引用。 - 这个引用是一个未定义的符号,编译器在后续的链接过程中会在其他源文件中查找这个符号的定义。
- 在链接期,链接器将多个目标文件合并成一个可执行文件,并且在当前源文件中声明的符号,会在其它源文件中找到对应的定义,并将它们链接起来。
下面是一个使用 extern
声明全局变量的示例:
// file2.cpp
int global_var = 42;//定义全局
// file1.cpp
#include <iostream>
extern int global_var;//声明其他文件的全局变量,使用extern进行链接
int main() {
std::cout << global_var << std::endl;
return 0;
}
- 在上面的示例中,
file1.cpp
文件中的main
函数使用了全局变量global_var
,但是global_var
的定义是在file2.cpp
中的,因此在file1.cpp
中需要使用extern
声明该变量。- 在编译时,编译器会为
global_var
生成一个符号表项,并在file1.cpp
中建立一个对该符号的引用。- 在链接时,链接器会在其他源文件中查找
global_var
的定义,并将其链接起来。
关于链接:
外部链接(External Linkage)
外部链接的符号可以在不同的源文件之间共享,并且在整个程序执行期间可见。全局变量和函数都具有外部链接。
内部链接(Internal Linkage)
内部链接的符号只能在当前源文件内部使用,不能被其他源文件访问。用 static
修饰的全局变量和函数具有内部链接。
无链接(No Linkage)
无链接的符号只能在当前代码块(函数或代码块)内部使用,不能被其他函数或代码块访问。用 const
或 constexpr
修饰的常量具有无链接属性( 通常情况下编译器是不会为const对象分配内存,也就无法链接)。
外部 C 链接(External C Linkage)
外部 C 链接的符号与外部链接类似,可以在不同的源文件之间共享,并且在整个程序执行期间可见。
它们具有 C 语言的名称和调用约定,可以与 C 语言编写的代码进行交互。
在 C++ 中,可以用
extern "C"
关键字来指定外部 C 链接,从而使用一些 C 的静态库。这些链接属性可以通过关键字
extern
、static
、const
和extern "C"
来显式地指定。
在实际的开发中,正确地理解和处理链接属性对于编写可重用、高效、可维护的代码非常重要。
extern c
extern "C" 的语法格式如下:
extern "C" {
// C 语言函数或变量的声明
}
采用entern c 声明的函数或者变量会采用C语言的连接规则,即符号和调用规则和C语言相同,也就是说,如果需要在C++代码中调用C语言的函数或者变量,则需要使用enxtern c 来进行调用和声明。
// C 语言代码
#include <stdio.h>
void print_message(const char* message) {
printf("%s\n", message);
}
// C++ 代码
extern "C" {
// 声明 C 语言函数
void print_message(const char* message);
}
int main() {
// 调用 C 语言函数
print_message("Hello, world!");
return 0;
}
在上面的代码中,使用 extern "C" 声明了 C 语言编写的 print_message 函数,使得它可以在 C++ 代码中被调用。
在 main 函数中,使用 C 语言的语法和命名规则来调用 print_message 函数,输出 "Hello, world!"。
- 需要注意 extern "C" 关键字只对函数的名称和调用约定起作用,对于函数的参数类型和返回值类型没有影响。
- 所以,在使用 extern "C" 声明函数时,需要保证函数的参数类型和返回值类型与 C 语言的定义相同,否则可能会导致编译错误或运行时错误。
mutable
- mutable是C++中的一个关键字,用于修饰类的成员变量,表示该成员变量即使在一个const成员函数中也可以被修改。
- mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词
- 因为在C++中,如果一个成员函数被声明为const,那么它不能修改类的任何成员变量,除非这个成员变量被声明为mutable。
- 看起来有点奇怪是不是,我也觉得,所以实际上这个关键字我在工作中几乎从来没看见过。
- 但是还是掌握以下吧哈哈
- 这个关键字主要应用场景就是:
- 如果需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置