C++常见问题整理

1.关键字

1.1 const

底层const指针:可以修改指向,不可以改值。

1
2
3
4
5
6
7
int dwVal1 = 0;
int dwVal2 = 0;

const int* p1 = &dwVal1;

p1 = &dwVal2; // 正确
*p1 = 5; // 不正确

底层const指针:可以改值,不可以改指向。

1
2
3
4
5
6
7
int dwVal1 = 0;
int dwVal2 = 0;

int* const p1 = &dwVal1;

p1 = &dwVal2; // 不正确
*p1 = 5; // 正确

成员函数指针:表示这个函数不会修改成员变量的内容,mutable关键字修饰的成员变量除外,同时该函数不能被static关键字修饰。

1
2
3
4
5
6
7
8
9
10
class A
{
public:
void m_fun1() const
{
m_dwVal1 = 5; // 错误
}
private:
int m_dwVal1;
};

1.2 static

static本质与全局符号的概念一致,只是在语法层面存在细微差别。

  • 修饰全局变量:语法层面限制该变量只在当前文件内有效。
  • 修饰全局函数:语法层面限制该函数只在当前文件内有效。
  • 修饰局部变量:语法层面用来扩展栈变量的生命周期。
  • 修饰成员变量:语法层面表示多个对象中共享数据,可以通过类名访问,需要在类外初始化,可以被普通成员函数访问。
  • 修饰成员函数:语法层面可以被类名直接调用,不传递this指针,不能访问普通成员变量。

1.3 virtual

用于声明一个类成员函数为虚函数。存在虚函数的类对象,首个成员固定为一个指针,指向该类的虚函数表。

当父类指针指向子类对象,子类重写父类虚函数,此时虚函数会以指针寻址的方式动态调用,构成多态性。

构造函数中完成虚表指针的初始化,所以构造函数不能被定义为虚函数。

析构函数声明为虚函数的原因:

1
2
Base* p = new A();
delete p;

如果析构函数不为虚函数,上面的代码会只调用父类的析构函数,子类的对象没有被正确释放。

当析构函数声明为虚函数时,会在子类虚函数表中找到子类的析构函数进行调用,先析构子类再析构父类,整个派生类被完全释放。

析构函数对比图

1.4 volatile

告诉编译器,这个变量的值需要每次都从内存中去读取,不允许暂存到寄存器中,可以用在多线程对某个全局标志的访问时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <Windows.h>

int g_Flag = 0;

DWORD ThreadProc1(
LPVOID lpThreadParameter
)
{
g_Flag = true;
while (g_Flag) {}
return 0;
}

DWORD ThreadProc2(
LPVOID lpThreadParameter
)
{
while (1)
{
::Sleep(5000);
g_Flag = false;
}
return 1;
}

int main()
{

HANDLE hThread1 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, 0);
HANDLE hThread2 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, 0);

return 0;
}

上面代码在Release版编译下,会死循环,汇编如下:

死循环

在将g_Flag使用volatile修饰后,不会产生死循环,汇编代码如下:

修复死循环

1.5 explict

用于修饰类构造函数,不允许对象发生隐式类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A
{
public:
explicit A(int a)
{
printf("A::constor\n");
}
};

int main()
{
A a = 2; // 错误
return 0;
}

2.字节对齐

2.1 规则

  • 结构体成员:第一个成员放在offset为0的位置,之后的每个数据成员按照#pragma pack指定的数值和sizeof()中的较小值进行对齐。
  • 结构体总体:总大小按照#pragma pack指定的数值和sizeof()中的较小值进行对齐。

2.2 原因

  • 平台原因:某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  • 性能原因:访问未对齐的内存,处理器需要作两次访问;而访问对齐的内存仅需要一次访问。

    如int类型的四字节存储在0x1地址上,cpu需要首先从0x0位置读4字节,舍去0x0这1个字节,再从0x4位置读4字节,舍去0x5开始的3个字节,最终拼接得到最终结果。

3.类型转换

  • const_cast

    用于对指针或引用去const属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int a = 5;
    const int b = 10;
    const int* pCa = &a;
    // *p = 10; // 错误
    // b= 20; // 错误
    int* pa = const_cast<int*>(pCa); // 指针去const
    int& rb = const_cast<int&>(b); // 引用去const

    *pa = 10; // 正确
    rb = 20; // 正确
  • static_cast

    与C风格强制类型转换基本一样,不存在运行时类型检查。

    1
    2
    float fVal1 = 3.1415926f;
    int dwVal2 = static_cast<int>(fVal1);
  • dynamic_cast

    具有运行时类型检查,可以防止在具有虚函数的派生关系中,将父类对象地址交给子类指针,会返回NULL。

    1
    2
    3
    4
    5
    6
    Base *base = new Base();
    Sub *pSub = dynamic_cast<Sub*>(base); // 转换失败,此时pSub为NULL
    if(pSub != NULL)
    {
    // do something.
    }
  • reinterpret_cast

    对要转换的二进制流重新解释,常用于指针与整数之间的转换。

    1
    2
    3
    4
    5
    unsigned short Hash(void *p) 
    {
    unsigned int val = reinterpret_cast<unsigned int>(p);
    return (unsigned short)(val ^ (val >> 16));
    }

C++常见问题整理
http://helloymf.github.io/2022/10/01/c-chang-jian-wen-ti-hui-zong/
作者
JNZ
许可协议