C++ 面试常见问题(C++基础)
资料整理自:https://csguide.cn/
C++ 中 sizeof 关键字
1、指针的大小永远是固定的,取决于处理器位数,32位就是 4 字节,64位就是 8 字节。
2、字符串数组要算上末尾的 '\0
int 占 4 个字节,double占 8 个字节,空的结构体也要占 1 个字节
// 前提: 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 和 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 是一个 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 类型的大小
}
C/C++中数组做参数退化为指针
sizeof 数组参数
C++ 面试中还有一个比较常见的考题,就是会将一个数组做参数,然后在函数内部用 sizeof 去判断这个数组参数的大小,如下:
int func(char array[]) {
printf("sizeof=%d\n", sizeof(array));
printf("strlen=%d\n", strlen(array));
}
int main() {
char array[] = "Hello World";
printf("sizeof=%d\n", sizeof(array));
printf("strlen=%d\n", strlen(array));
func(array);
}
数组退化:在 C++ 中,数组在作为函数参数时会退化为指向其首元素的指针。
退化的原因是因为数组作为函数参数时,实际传递的是指向数组首元素的指针,不可能逐个拷贝整个数组然后在栈上传递,所以编译器只知道参数是一个指针,而不知道它的长度信息。
但是,当数组直接作为 sizeof 的参数时,它不会退化,因为 sizeof 是编译器在编译期间计算的结果,这个时候编译器是有信息知道数组的大小。
为了在函数中获取数组的长度,需要将数组的长度作为另一个参数传递给函数,或者使用模板实现。
#include <iostream>
#include <cstring>
template <typename T, std::size_t N>
void printSizeAndLength(const T (&arr)[N]) {
std::cout << "Size of arr in function: " << sizeof(arr) << std::endl; // 计算数组的大小
std::cout << "Length of arr: " << strlen(arr) << std::endl; // 计算字符串的长度
}
int main() {
char str[] = "Hello, world!";
std::cout << "Size of str in main: " << sizeof(str) << std::endl; // 计算整个字符数组的大小
printSizeAndLength(str);
}
Size of str in main: 14
Size of arr in function: 14
Length of arr: 13
这段代码使用了模板函数 printSizeAndLength,它接受一个数组引用作为参数。
在函数内部,使用 sizeof 计算数组的大小时,数组不会退化为指针。
引用的作用就在于阻止拷贝的发生,通过传递引用,让形参得到和数组名同样的地址。
C++ 中 const 关键字
当 const 修饰变量时,该变量将被视为只读变量,即不能被修改。
对于确定不会被修改的变量,应该加上 const,这样可以保证变量的值不会被无意中修改,也可以使编译器在代码优化时更加智能。
例如:
const int a = 10;
a = 20; // 编译错误,a 是只读变量,不能被修改
对 const int 类型取指针,就是 const int* 类型的指针,将其强制转换为 int* 类型,就去掉了 const 限制,从而修改变量的值。
在 C++ 中,将 const 类型的指针强制转换为非const 类型的指针被称为类型强制转换(Type Casting),这种行为称为 const_cast。
下面👇这个例子,展示了使用 const_cast 修改 const变量的值却不会起作用:
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*
在上面的例子中,将 p 声明为 const int* 类型,指向只读变量 a 的地址。
然后使用 const_cast 将 p 强制转换为 int* 类型的指针 q,从而去掉了 const限制。
接下来,通过指针 q 间接修改了变量 a 的值。
但是请注意,即使 a 的值被修改了,但在程序中输出a 的值仍然是 10,
正如前面所有,因为 a 是只读变量,所以编译器做了优化,早就把代码实际替换为了👇下面这样:
std::cout << "a = " << 10 << std::endl;
当 const 修饰函数参数时,表示函数内部不会修改该参数的值。这样做可以使代码更加安全,避免在函数内部无意中修改传入的参数值。
尤其是 引用 作为参数时,如果确定不会修改引用,那么一定要使用 const 引用。
例如:
void func(const int a) {
// 编译错误,不能修改 a 的值
a = 10;
}
当 const 修饰函数返回值时,表示函数的返回值为只读,不能被修改。这样做可以使函数返回的值更加安全,避免被误修改。
例如:
const int func() {
int a = 10;
return a;
}
int main() {
const int b = func(); // b 的值为 10,不能被修改
b = 20; // 编译错误,b 是只读变量,不能被修改
return 0;
}
在 C/C++ 中,const 关键字可以用来修饰指针,用于声明指针本身为只读变量或者指向只读变量的指针。
根据 const 关键字的位置和类型,可以将 const 指针分为以下三种情况:
#4.1. 指向只读变量的指针
这种情况下,const 关键字修饰的是指针所指向的变量,而不是指针本身。
因此,指针本身可以被修改(意思是指针可以指向新的变量),但是不能通过指针修改所指向的变量。
const int* p; // 声明一个指向只读变量的指针,可以指向 int 类型的只读变量
int a = 10;
const int b = 20;
p = &a; // 合法,指针可以指向普通变量
p = &b; // 合法,指针可以指向只读变量
*p = 30; // 非法,无法通过指针修改只读变量的值
4.2 只读指针
这种情况下,const 关键字修饰的是指针本身,使得指针本身成为只读变量。
因此,指针本身不能被修改(即指针一旦初始化就不能指向其它变量),但是可以通过指针修改所指向的变量。
int a = 10;
int b = 20;
int* const p = &a; // 声明一个只读指针,指向 a
*p = 30; // 合法,可以通过指针修改 a 的值
p = &b; // 非法,无法修改只读指针的值
4.3 只读指针指向只读变量
这种情况下,const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。
因此,指针本身不能被修改,也不能通过指针修改所指向的变量。
const int a = 10;
const int* const p = &a; // 声明一个只读指针,指向只读变量 a
*p = 20; // 非法,无法通过指针修改只读变量的值
p = nullptr; // 非法,无法修改只读指针的值
#4.4 常量引用
常量引用是指引用一个只读变量的引用,因此不能通过常量引用修改变量的值。
const int a = 10;
const int& b = a; // 声明一个常量引用,引用常量 a
b = 20; // 非法,无法通过常量引用修改常量 a 的值
当 const 修饰成员函数时,表示该函数不会修改对象的状态(就是不会修改成员变量)。
这样有个好处是,const 的对象就可以调用这些成员方法了,因为 const 对象不允许调用非 const 的成员方法。
处。
class A {
public:
int func() const {
// 编译错误,不能修改成员变量的值
m_value = 10;
return m_value;
}
private:
int m_value;
};
C++ 中 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
}
static 修饰局部变量可以使得变量在函数调用结束后不会被销毁,而是一直存在于内存中,下次调用该函数时可以继续使用。
同时,由于 static 修饰的局部变量的作用域仅限于函数内部,所以其他函数无法访问该变量。
void foo() {
static int count = 0; // static 修饰局部变量
count++;
cout << count << endl;
}
int main() {
foo(); // 输出 1
foo(); // 输出 2
foo(); // 输出 3
return 0;
}
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 函数
}
static 修饰类成员变量和函数可以使得它们在所有类对象中共享,且不需要创建对象就可以直接访问。
class MyClass {
public:
static int count; // static 修饰类成员变量
static void foo() { // static 修饰类成员函数
cout << count << endl;
}
};
// 访问:
MyClass::count;
MyClass::foo();
C++ 中 volatile 的作用
作用
volatile是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。
当一个变量被声明为volatile时,编译器会禁止对该变量进行优化,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
volatile int counter = 0;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
counter++;
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 创建两个线程,分别执行increment函数
pthread_create(&thread1, NULL, increment, NULL);
pthread_create(&thread2, NULL, increment, NULL);
// 等待两个线程执行完毕
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Counter: %d\n", counter);
return 0;
}
上面声明了一个volatile int类型的全局变量counter,并创建了两个线程。
每个线程都会对counter变量进行100000次自增操作。
由于counter变量被声明为volatile,编译器不会对其进行优化,确保每次访问都会从内存中读取值。
当然啦,即便是volatile关键字可以确保编译器不对变量进行优化,但上面任然存在并发问题,counter++操作仍然可能导致数据不一致。
为了解决这个问题,需要使用互斥锁、原子操作或其他同步机制。
C/C++ 字节对齐
在C/C++中,字节对齐是内存分配的一种策略。
当分配内存时,编译器会自动调整数据结构的内存布局,使得数据成员的起始地址与其自然对齐边界(一般为自己大小的倍数)相匹配。
#字节对齐的作用和好处
计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,就像下面这样,
由于 1 个 bit 只能表示两个状态,所以大佬们规定 8个 bit 为一组,命名为 byte。
并且将 byte 作为内存寻址的最小单元,也就是给每个 byte 一个编号,这个编号就叫内存的地址。
理论上,任何类型的变量都可以从任意地址开始存放。
然而实际上,访问特定类型的变量通常需要从特定对齐的内存地址开始。
因为如果不对数据存储进行适当的对齐,可能会导致存取效率降低。
所以,各种数据类型需要按照一定的规则在内存中排列(起始地址),而不是顺序地一个接一个排放,这种排列就是字节对齐。
例如,有些平台每次读取都是从偶数地址开始。如果一个 int 类型(假设为 32 位系统)存储在偶数地址开始的位置,那么一个读周期就可以读取这 32 位。
但如果存储在奇数地址开始的位置,则需要两个读周期,并将两次读取的结果的高低字节拼凑才能得到这 32 位数据。
显然这会显著降低读取效率。
总结: 字节对齐有助于提高内存访问速度,因为许多处理器都优化了对齐数据的访问。但是,这可能会导致内存中的一些空间浪费。
#字节对齐规则
以下是字节对齐的一些基本规则:
#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;
}
C/C++ 字节序
面试高频指数:★★★★☆
字节序是指在多字节数据类型(如整数、浮点数等)中,字节在内存中的存储顺序。
主要有两种字节序:大端字节序(Big-endian)和小端字节序(Little-endian)。
#大端字节序(Big-endian)
高位字节存储在低地址处,低位字节存储在高地址处。例如,一个4字节的整数0x12345678,在大端字节序的系统中,内存布局如下(从左侧的低地址到右侧的高地址):
0x12 | 0x34 | 0x56 | 0x78
大端字节序是符合人类阅读习惯的顺序。
#小端字节序(Little-endian)
低位字节存储在低地址处,高位字节存储在高地址处。
例如,一个4字节的整数0x12345678,在小端字节序的系统中,内存布局如下(从左侧的低地址到右侧的高地址):
0x78 | 0x56 | 0x34 | 0x12
判断系统的字节序的方法有多种,下面是一个简单的 C++ 代码示例:
#include <iostream>
int main() {
int num = 1;
// 将int类型指针转换为char类型指针,取第一个字节
char* ptr = reinterpret_cast<char*>(&num);
if (*ptr == 1) {
std::cout << "Little-endian" << std::endl;
} else {
std::cout << "Big-endian" << std::endl;
}
return 0;
}
这段代码的原理就是,整数num值初始化为1(0x00000001)。然后将其指针类型从int转换为char,这样我们就可以访问该整数的第一个字节。
如果系统是小端字节序,那么第一个字节是1;如果系统是大端字节序,那么第一个字节是0。
通过判断第一个字节的值,我们就可以得知系统的字节序。
常见的大小端字节序
在计算机领域中,不同的系统、平台和协议使用不同的字节序。下面是一些常见情况的字节序:
#1. 网络传输
在网络传输过程中,通常使用大端字节序(Big-endian),也称为网络字节序,这是 TCP/IP 协议的规定,多字节数据在网络上传输时使用大端字节序。
因此,如果本地系统使用的是小端字节序,那么就需要在传输之前将其转换为大端字节序。
一般通过使用htonl()、htons()、ntohl()和ntohs()等函数来完成。
#2. Linux
Linux 操作系统在不同的硬件平台上可能使用不同的字节序。例如,x86 和 x86_64(Intel 和 AMD 处理器)是小端字节序(Little-endian),而 PowerPC 和 SPARC 等其他架构可能使用大端字节序(Big-endian)。
所以具体的字节序取决于运行 Linux 的硬件平台。
#3. Windows
Windows 操作系统主要运行在 x86 和 x86_64(Intel 和 AMD处理器)架构上,这些处理器使用小端字节序(Little-endian)。
#4. Mac
一般使用 Intel 处理器或 Apple 自家的 M1 芯片(基于ARM架构),这些处理器都采用小端字节序(Little-endian)。
#5. 总结
在网络传输中,通常使用大端字节序(网络字节序)。
在具体的操作系统中,字节序取决于底层硬件架构。例如,Linux和Windows操作系统主要运行在x86和x86_64(Intel和AMD处理器)架构上,这些处理器使用小端字节序。
而其他硬件平台,如PowerPC和SPARC等,可能使用大端字节序。
C++ 中 class 和 struct 区别
面试高频指数:★★★☆☆
C++ 中为了兼容 C 语言而保留了 C 语言的 struct 关键字,并且加以扩充了含义。
在 C 语言中,struct 只能包含成员变量,不能包含成员函数。
而在 C++ 中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
不同点
C++ 中的 struct 和 class 基本是通用的,唯有几个细节不同:
class 中类中的成员默认都是 private 属性的。
而在 struct 中结构体中的成员默认都是 public 属性的。
class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
class 可以用于定义模板参数,struct 不能用于定义模板参数。
意思是这样可以:
template <class T>
struct Person {
public:
T age;
};
但是这样就会编译报错:
template <struct T>
struct Person {
public:
T age;
};
实际使用中,struct 我们通常用来定义一些 POD(plain old data)
POD是 C++ 定义的一类数据结构概念,比如 int、float 等都是 POD 类型的。
Plain 代表它是一个普通类型,Old 代表它是旧的,与几十年前的 C 语言兼容,那么就意味着可以使用 memcpy() 这种最原始的函数进行操作。
两个系统进行交换数据,如果没有办法对数据进行语义检查和解释,那就只能以非常底层的数据形式进行交互,而拥有 POD 特征的类或者结构体通过二进制拷贝后依然能保持数据结构不变。
也就是说,能用 C 的 memcpy() 等函数进行操作的类、结构体就是 POD 类型的数据。
而 class 用于定义一些 非 POD 的对象,面向对象编程。
C++ 宏定义(define)和内联函数(inline)的区别
宏定义(#define)和内联函数(inline)都是为了减少函数调用开销和提高代码运行效率而引入的机制,但是它们的实现方式和作用机制略有不同。
#define
在 C/C++ 中,#define 是预处理指令的一种。
一般用于定义宏(macro),主要有两种用途:
-
定义常量
-
创建宏函数
无论哪种都是用于在编译时替换文本,也就是 define 实际上只是做文本的替换,可以使用 gcc -E 选项查看宏替换后的结果。
本篇我们主要讲宏函数的场景,举例:
#define SQUARE_SUM(x, y) ((x) * (x) + (y) * (y))
写宏函数一定要注意:参数和函数体应当用括号包围,避免因运算优先级导致的错误
inline
内联函数的定义和普通函数类似,只需在函数声明前加上 inline 关键字即可。
但是编译器并不一定会将所有声明为内联函数的函数都进行内联,是否内联取决于编译器的实现和优化策略。
内联函数的优点是类型安全、可调试、可优化,但是也存在一些问题。
由于函数体会被复制多次,会占用更多的代码段空间,而且在某些情况下可能会导致代码膨胀。
我们再从以下角度对比下宏定义和内联函数:
差别
#1. 语义
宏定义使用预处理器指令 #define 定义。
它在编译期间将宏展开,并替换宏定义中的代码。
预处理器只进行简单的文本替换,不涉及类型检查。
内联函数使用 inline 关键字定义,它是一个真正的函数。
编译器会尝试将内联函数的调用处用函数体进行替换,从而避免函数调用的开销。
#2. 类型检查:
宏定义就是单纯的字符替换,不涉及类型检查,容易导致错误。
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
double b = 5.5;
// 这里没有类型检查,但在运行时可能导致问题
double result = SQUARE(b);
}
内联函数会进行类型检查,更加安全。
inline int square(int x) {
return x * x;
}
int main() {
int a = 5;
double b = 5.5;
// 下面这行代码将导致编译错误,因为类型不匹配
double result = square(b);
}
#define MAX(a, b) ((a) > (b) ? (a) : (c))
int x = 1;
int y = MAX(x++, 10);
// 宏定义 x++ 会被执行两次, 因为 x++ 会被替换到两个地方,使用内联函数则不会出现这个问题
在 inline 函数传递参数只计算一次,而在使用宏定义的情况下,每次在程序中使用宏时都会计算表达式参数,因此宏会对表达式参数计算多次。 因为宏只是做替换,可能会把同样的表达式替换到多个地方。
宏定义 (define) 和 typedef 的区别
面试高频指数:★★☆☆☆
宏定义(#define)和 typedef 都是 C++ 语言中用于定义别名的方法,但它们有一些关键区别:
#区别
宏定义 #define 在编译期间将宏展开,并替换宏定义中的代码。
预处理器只进行简单的文本替换,不涉及类型检查。
比如:
#define INT_VECTOR std::vector<int>
typedef 是一种类型定义关键字,用于为现有类型创建新的名称(别名)。
与宏定义不同,typedef 是在编译阶段处理的,有更严格的类型检查。
typedef std::vector<int> IntVector;
宏定义没有作用域限制,只要在宏定义之后的地方,就可以使用宏。
通常用于定义常量、简单的表达式或简单的代码片段。
typedef 遵循 C++ 的作用域规则,可以受到命名空间、类等结构的作用域限制。
typedef 通常用于定义复杂类型的别名,使代码更易读和易于维护,如:
typedef std::map<std::string, std::vector<int>> StringToIntVectorMap;
宏定义不支持模板,因此不能用于定义模板类型别名。
typedef 可以与模板结合使用,但在 C++11 之后,推荐使用 using 关键字定义模板类型别名。
// 使用 typedef 定义模板类型别名
template <typename T>
struct MyContainer {
typedef std::vector<T> Type;
};
// 使用 using 定义模板类型别名(C++11 及以后)
template <typename T>
struct MyContainer {
using Type = std::vector<T>;
};
C++ 中 explicit 的作用
面试高频指数:★★★☆☆
在 C++ 中,explicit 通常用于构造函数的声明中,用于防止隐式转换。 当将一个参数传递给构造函数时,如果构造函数声明中使用了 explicit 关键字,则只能使用显式转换进行转换,而不能进行隐式转换。
举个例子:
int a = 0;
long b = a + 1; // int 转换为 long
if (a == b) {
// 默认的operator==需要a的类型和b相同,因此也发生转换
}
explicit 的作用
接下来通过一个例子来详细解释 explicit 的作用。
有一个类 MyInt,表示一个整数,并且有一个构造函数可以将 int 类型的参数转换为
MyInt 类型
class MyInt {
public:
MyInt(int n) : num(n) {}
private:
int num;
};
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));
C++ 中 extern 作用
面试高频指数:★★★★☆
一般而言,C++全局变量的作用范围仅限于当前的文件,但同时C++也支持分离式编译,允许将程序分割为若干个文件被独立编译。
于是就需要在文件间共享变量数据,这里extern就发挥了作用。
extern 用于指示变量或函数的定义在另一个源文件中,并在当前源文件中声明。 说明该符号具有外部链接(external linkage)属性。
首先明白 C/C++ 中变量的声明和定义是两个不同的概念。 声明是指告诉编译器某个符号的存在,在程序变量表中记录类型和名字,而定义则是指为该符号分配内存空间或实现其代码逻辑。
凡是没有带extern的声明同时也都是定义。 而对函数而言,带有{}是定义,否则是声明。如果想声明一个变量而非定义它,就在变量名前添加关键字extern,且不要显式的初始化变量。
1.1 变量的声明与定义
// 声明
extern int global_var;
// 定义
int global_var = 42;
在上面的示例中,global_var 变量的声明使用 extern 关键字告诉编译器它的定义在其他源文件中,而定义则是为变量分配内存空间并初始化为 42。
#1.2 函数的声明和定义:
// 声明
int sum(int a, int b);
// 定义
int sum(int a, int b) {
return a + b;
}
在上面的示例中,sum 函数的声明告诉编译器该函数的存在及其参数和返回值类型,而定义则是实现函数的代码逻辑。
2. C/C++ 中链接属性
在 C++ 中,链接属性是指程序在编译、链接和执行阶段如何处理符号(变量、函数、类等)的可见性和重复定义。 C++ 语言规定有以下链接属性:
#2.1. 外部链接(External Linkage)
外部链接的符号可以在不同的源文件之间共享,并且在整个程序执行期间可见。全局变量和函数都具有外部链接。
#2.2 内部链接(Internal Linkage)
内部链接的符号只能在当前源文件内部使用,不能被其他源文件访问。用 static 修饰的全局变量和函数具有内部链接。
#2.3 无链接(No Linkage)
无链接的符号只能在当前代码块(函数或代码块)内部使用,不能被其他函数或代码块访问。用 const 或 constexpr 修饰的常量具有无链接属性( 通常情况下编译器是不会为const对象分配内存,也就无法链接)。
#2.4 外部 C 链接(External C Linkage)
外部 C 链接的符号与外部链接类似,可以在不同的源文件之间共享,并且在整个程序执行期间可见。
它们具有 C 语言的名称和调用约定,可以与 C 语言编写的代码进行交互。
在 C++ 中,可以用 extern “C” 关键字来指定外部 C 链接,从而使用一些 C 的静态库。
这些链接属性可以通过关键字 extern、static、const 和 extern “C” 来显式地指定。
3. extern 作用
3.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;
}
3.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.3 编译和链接过程
编译链接过程中,extern 的作用如下:
在编译期,extern 用于告诉编译器某个变量或函数的定义在其他源文件中,编译器会为它生成一个符号表项,并在当前源文件中建立一个对该符号的引用。
这个引用是一个未定义的符号,编译器在后续的链接过程中会在其他源文件中查找这个符号的定义。
在链接期,链接器将多个目标文件合并成一个可执行文件,并且在当前源文件中声明的符号,会在其它源文件中找到对应的定义,并将它们链接起来。
下面是一个使用 extern 声明全局变量的示例:
// file1.cpp
#include <iostream>
extern int global_var;
int main() {
std::cout << global_var << std::endl;
return 0;
}
// file2.cpp
int global_var = 42;
在上面的示例中,file1.cpp 文件中的 main 函数使用了全局变量 global_var,但是 global_var 的定义是在 file2.cpp 中的,因此在 file1.cpp 中需要使用 extern 声明该变量。
在编译时,编译器会为 global_var 生成一个符号表项,并在 file1.cpp 中建立一个对该符号的引用。
在链接时,链接器会在其他源文件中查找 global_var 的定义,并将其链接起来。
extern C 的作用
面试高频指数:★★★★☆
正如这篇文章extern 的作用 (opens new window)所说, extern 是指示链接可见性和符号规则,而 extern “C” 则是 C++ 语言提供的一种机制,用于在 C++ 代码中调用 C 语言编写的函数和变量。
函数的命名规则
简单解释一下什么是函数的命名规则:对于 C++ 语言,由于需要支持重载,所以一个函数的链接名(Linkage Name)是由函数的名称、参数类型和返回值类型等信息组成的,用于在编译和链接时唯一标识该函数。
函数的链接名的生成规则在不同的编译器和操作系统上可能有所不同,一般是由编译器自动处理,不需要手动指定,这个规则常常叫做Name Mangling(opens new window)
下面介绍一些常见的规则:
Microsoft Visual C++ 编译器(Windows):函数的名称会被编译器修改为一个以 “_” 开头的名称,并加上参数类型和返回值类型等信息,以避免链接冲突。例如,函数 int add(int a, int b) 的链接名可能是 _add_int_int。
GCC 编译器(Linux):也会加上参数类型和返回值类型等信息。例如,函数 int add(int a, int b) 的链接名可能是 _Z3addii。
Clang 编译器(MacOS):函数的链接名的生成规则与 GCC 编译器类似,但稍有不同。例如,函数 int add(int a, int b) 的链接名可能是 _Z3addii。
而 C 语言的链接函数名规则又和 上面三个 C++ 不一样,通过在 C++ 代码中使用 extern “C” 关键字,可以将 C++ 编译器的命名规则转换为 C 语言的命名规则,从而使得 C++ 代码可以调用 C 语言的函数或变量。
extern c 语法
extern “C” 的语法格式如下:
extern "C" {
// 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来修饰,并且放在函数后后面关键字位置。
#include <iostream>
class Counter {
public:
Counter() : count(0), cache_valid(false), cached_value(0) {}
int get_count() const {
if (!cache_valid) {
// 模拟一个耗时的计算过程
cached_value = count * 2;
cache_valid = true;
}
return cached_value;
}
void increment() {
count++;
cache_valid = false; // 使缓存无效,因为count已经更改
}
private:
int count;
mutable bool cache_valid; // 缓存是否有效的标志
mutable int cached_value; // 缓存的值
};
int main() {
Counter counter;
counter.increment();
counter.increment();
std::cout << "Count: " << counter.get_count() << std::endl; // 输出 4
return 0;
}
上面定义了一个Counter类,该类具有一个计数成员变量count。还有两个mutable成员变量:cache_valid和cached_value。
这两个变量用于在get_count函数中缓存计算结果,从而提高性能。
get_count函数被声明为const,因为它在逻辑上不会更改类的状态。
然而,需要更新cache_valid和cached_value变量以提高性能。
为了在const成员函数中修改这两个变量,将它们声明为mutable。
这个例子不那么贴切的展示了mutable关键字的用途:
即允许在const成员函数中修改特定的成员变量,以支持内部实现所需的功能,同时仍然保持外部不变性。
C++ 几种类型转换
面试高频指数:★★★☆☆
在 C 语言中,大多数是用 (type_name) expression 这种方式来做强制类型转换,但是在 C++ 中,更推荐使用四个转换操作符来实现显式类型转换:
static_cast
dynamic_cast
const_cast
reinterpret_cast
一、static_cast
用法: static_cast <new_type> (expression)
其实 static_cast 和 C 语言 () 做强制类型转换基本是等价的。
主要用于以下场景:
#1.1 基本类型之间的转换
将一个基本类型转换为另一个基本类型,例如将整数转换为浮点数或将字符转换为整数。
int a = 42;
double b = static_cast<double>(a); // 将整数a转换为双精度浮点数b
1.2 指针类型之间的转换
将一个指针类型转换为另一个指针类型,尤其是在类层次结构中从基类指针转换为派生类指针。这种转换不执行运行时类型检查,可能不安全,要自己保证指针确实可以互相转换。
class Base {};
class Derived : public Base {};
Base* base_ptr = new Derived();
Derived* derived_ptr = static_cast<Derived*>(base_ptr); // 将基类指针base_ptr转换为派生类指针derived_ptr
1.3 引用类型之间的转换
类似于指针类型之间的转换,可以将一个引用类型转换为另一个引用类型。在这种情况下,也应注意安全性。
Derived derived_obj;
Base& base_ref = derived_obj;
Derived& derived_ref = static_cast<Derived&>(base_ref); // 将基类引用base_ref转换为派生类引用derived_ref
二、dynamic_cast
用法: dynamic_cast <new_type> (expression)
dynamic_cast在C++中主要应用于父子类层次结构中的安全类型转换。
它在运行时执行类型检查,因此相比于static_cast,它更加安全。
dynamic_cast的主要应用场景:
2.1 向下类型转换
当需要将基类指针或引用转换为派生类指针或引用时,dynamic_cast可以确保类型兼容性。
如果转换失败,dynamic_cast将返回空指针(对于指针类型)或抛出异常(对于引用类型)。
class Base { virtual void dummy() {} };
class Derived : public Base { int a; };
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr); // 将基类指针base_ptr转换为派生类指针derived_ptr
2.2 用于多态类型检查
处理多态对象时,dynamic_cast可以用来确定对象的实际类型,例如:
class Animal { public: virtual ~Animal() {} };
class Dog : public Animal { public: void bark() { /* ... */ } };
class Cat : public Animal { public: void meow() { /* ... */ } };
Animal* animal_ptr = /* ... */;
// 尝试将Animal指针转换为Dog指针
Dog* dog_ptr = dynamic_cast<Dog*>(animal_ptr);
if (dog_ptr) {
dog_ptr->bark();
}
// 尝试将Animal指针转换为Cat指针
Cat* cat_ptr = dynamic_cast<Cat*>(animal_ptr);
if (cat_ptr) {
cat_ptr->meow();
}
另外,要使用dynamic_cast有效,基类至少需要一个虚拟函数。
因为,dynamic_cast只有在基类存在虚函数(虚函数表)的情况下才有可能将基类指针转化为子类。
2.3 dynamic_cast 底层原理
dynamic_cast的底层原理依赖于运行时类型信息(RTTI, Runtime Type Information)。
C++编译器在编译时为支持多态的类生成RTTI,它包含了类的类型信息和类层次结构。
我们都知道当使用虚函数时,编译器会为每个类生成一个虚函数表(vtable),并在其中存储指向虚函数的指针。
伴随虚函数表的还有 RTTI(运行时类型信息),这些辅助的信息可以用来帮助我们运行时识别对象的类型信息。
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream& os) const;
float _x;
static int _point_count;
};
首先,每个多态对象都有一个指向其vtable的指针,称为vptr。
RTTI(就是上面图中的 type_info 结构)通常与vtable关联。
dynamic_cast就是利用RTTI来执行运行时类型检查和安全类型转换。
以下是dynamic_cast的工作原理的简化描述:
首先,dynamic_cast通过查询对象的 vptr 来获取其RTTI(这也是为什么 dynamic_cast 要求对象有虚函数)
然后,dynamic_cast比较请求的目标类型与从RTTI获得的实际类型。如果目标类型是实际类型或其基类,则转换成功。
如果目标类型是派生类,dynamic_cast会检查类层次结构,以确定转换是否合法。如果在类层次结构中找到了目标类型,则转换成功;否则,转换失败。
当转换成功时,dynamic_cast返回转换后的指针或引用。
如果转换失败,对于指针类型,dynamic_cast返回空指针;对于引用类型,它会抛出一个std::bad_cast异常。
因为dynamic_cast依赖于运行时类型信息,它的性能可能低于其他类型转换操作(如static_cast),static 是编译器静态转换,编译时期就完成了。
三、const_cast
用法: const_cast <new_type> (expression) new_type 必须是一个指针、引用或者指向对象类型成员的指针。
#3.1 修改const对象
当需要修改const对象时,可以使用const_cast来删除const属性。
const int a = 42;
int* mutable_ptr = const_cast<int*>(&a); // 删除const属性,使得可以修改a的值
*mutable_ptr = 43; // 修改a的值
3.2 const对象调用非const成员函数
当需要使用const对象调用非const成员函数时,可以使用const_cast删除对象的const属性。
class MyClass {
public:
void non_const_function() { /* ... */ }
};
const MyClass my_const_obj;
MyClass* mutable_obj_ptr = const_cast<MyClass*>(&my_const_obj); // 删除const属性,使得可以调用非const成员函数
mutable_obj_ptr->non_const_function(); // 调用非const成员函数
四、reinterpret_cast
用法: reinterpret_cast <new_type> (expression)
reinterpret_cast用于在不同类型之间进行低级别的转换。
4.1 指针类型之间的转换
在某些情况下,需要在不同指针类型之间进行转换,如将一个int指针转换为char指针。
这在 C 语言中用的非常多,C语言中就是直接使用 () 进行强制类型转换
int a = 42;
int* int_ptr = &a;
char* char_ptr = reinterpret_cast<char*>(int_ptr); // 将int指针转换为char指针
t的工作原理的简化描述:
首先,dynamic_cast通过查询对象的 vptr 来获取其RTTI(这也是为什么 dynamic_cast 要求对象有虚函数)
然后,dynamic_cast比较请求的目标类型与从RTTI获得的实际类型。如果目标类型是实际类型或其基类,则转换成功。
如果目标类型是派生类,dynamic_cast会检查类层次结构,以确定转换是否合法。如果在类层次结构中找到了目标类型,则转换成功;否则,转换失败。
当转换成功时,dynamic_cast返回转换后的指针或引用。
如果转换失败,对于指针类型,dynamic_cast返回空指针;对于引用类型,它会抛出一个std::bad_cast异常。
因为dynamic_cast依赖于运行时类型信息,它的性能可能低于其他类型转换操作(如static_cast),static 是编译器静态转换,编译时期就完成了。
三、const_cast
用法: const_cast <new_type> (expression) new_type 必须是一个指针、引用或者指向对象类型成员的指针。
#3.1 修改const对象
当需要修改const对象时,可以使用const_cast来删除const属性。
const int a = 42;
int* mutable_ptr = const_cast<int*>(&a); // 删除const属性,使得可以修改a的值
*mutable_ptr = 43; // 修改a的值
3.2 const对象调用非const成员函数
当需要使用const对象调用非const成员函数时,可以使用const_cast删除对象的const属性。
class MyClass {
public:
void non_const_function() { /* ... */ }
};
const MyClass my_const_obj;
MyClass* mutable_obj_ptr = const_cast<MyClass*>(&my_const_obj); // 删除const属性,使得可以调用非const成员函数
mutable_obj_ptr->non_const_function(); // 调用非const成员函数
四、reinterpret_cast
用法: reinterpret_cast <new_type> (expression)
reinterpret_cast用于在不同类型之间进行低级别的转换。
4.1 指针类型之间的转换
在某些情况下,需要在不同指针类型之间进行转换,如将一个int指针转换为char指针。
这在 C 语言中用的非常多,C语言中就是直接使用 () 进行强制类型转换
int a = 42;
int* int_ptr = &a;
char* char_ptr = reinterpret_cast<char*>(int_ptr); // 将int指针转换为char指针