为何多态只能由指针或引用来实现
指针和引用(通常以指针来实现)的大小是固定的(一个 word),而对象的大小却是可变的。其类的指针和引用可以指向(或引用)子类,但是基类的对象永远也只能是基类,没有变化则不可能引发多态。
字符串数组内存表示
string a = "qyert";
char b[] = "qyert";
// 与string a = "qyert";一致
string a;
a.push_back('q');
a.push_back('y');
a.push_back('e');
a.push_back('r');
a.push_back('t');
const
- 指针所指向的内容不可变:
int const * p
和const int * p
。const
放置在int
前后都一样。 - 指针本身不可变:
int * const p
。const
修饰了后面的p
这个指针。
参考:用最好的方法去理解const int, int const以及int *const
栈溢出、堆溢出
栈溢出:超出栈空间大小。堆栈溢出通常会造成操作系统产生SIGSEGV信号。
导致栈溢出的操作:1、递归过深 2、局部变量过大 3、向过小的局部变量中写入过大的数据
栈工作原理
其中最重要的两个寄 存器是ebp和esp(32位),ebp(extended base pointer),即扩展的基址指针寄存器,听名字就知道是干啥了的,esp(extended stack pointer),即扩展的栈顶指针寄存器。这两个寄存器是配对使用的,esp在扩展空间后会减小(栈是逆向生长的哈),但是ebp会始终指向栈的底部,为啥要指在底部不动?因为esp是变化的量,如果你要访问栈中的数据,当然是使用一个不变的量ebp+偏移地址要方便的多。
当你要初始化一个局部变量的时候,esp指针就会向下开辟空间,再将你的数据移入,之后的访问会使用ebp+变量的大小进行访问。而当你要调用一个函数时,会进行以下步骤:
- 先将你的实参以从右到左的方式压入栈中
- 将下一条语句的地址压入栈中
- 将当前ebp压入栈中,即调用函数的old ebp入栈中
子函数被调用时,子函数当前的ebp寄存器指向了调用函数的ebp,即old ebp(previous ebp)。而当子函数执行完毕后,就会将子函数的esp重新指向子函数的ebp,再弹出ebp恢复父函数的ebp,接着弹出返回地址到eip继续执行父函数的代码。最终的结构大概是这样的(main函数中调用子函数foo(3, 4)):
内存布局与栈溢出&堆溢出原理
访问空指针
在Linux系统上,访问空指针会产生 Segmentation fault 的错误。
访问指针的时候虚拟地址就会向物理地址映射,此时页表会去查看这块地址,而这块地址被存放在只读区,当页表发现地址是无效的,就会反映给操作系统,操作系统就会发送11号信号终止此进程,所以进程异常终止程序崩溃。
让我们假设现在使用的是 C 语言,运行在 Linux 系统上,以此来分析访问 NULL 指针的过程。
- Linux 中,每个进程空间的 0x0 虚拟地址开始的线性区(memory region)都会被映射到一个用户态没有访问权限的页上。通过这样的映射,内核可以保证没有别的页会映射到这个区域。
- 编译器把空指针当做 0 对待,开心地让你去访问空指针。
- 缺页异常处理程序被调用,因为在 0x0 的页没有在物理内存里面。
- 缺页异常处理程序发现你没有访问的权限。
-
内核发送SIGSEGV(
SEGV是segmentation violation*(段违例)的缩写
)信号给进程,该信号默认是让进程自杀。
可以看到:不需要特定的编译器实现或者内核的支持,只需要让一个页映射到 0x0 的虚拟地址上,就完美的实现了检测空指针的错误。
sizeof
- sizeof空结构体,为1,也占用1字节的内存。
- sizeof函数时:其结果是函数返回值类型的大小,函数并不会被调用。对函数求值的形式:sizeof(函数名(实参表))
注意:1)不可以对返回值类型为空的函数求值。
2)不可以对函数名求值。
3)对有参数的函数,在用sizeof时,须写上实参表 - sizeof数组时:计算的是数组所占用的内存字节数。1)当字符数组表示字符串时,其sizeof值将’/0’计算进去。 2)当数组为形参时,其sizeof值相当于指针的sizeof值。
char a[10];
char n[] = "abc";
cout<<sizeof(a)<<endl; // 数组,值为10
cout<<sizeof(n)<<endl; // 字符串数组,将'/0'计算进去,值为4
void func(char a[3])
{
int c = sizeof(a); //c = 4,因为这里a不在是数组类型,而是指针,相当于char *a。
}
C++类型转换
C++内存模型
C++多态实现原理——虚函数表
- 多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定。
- 每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针(可以认为这是由编译器自动添加到构造函数中的指令完成的)。
- 虚函数表是编译器生成的,程序运行时被载入内存。一个类的虚函数表中列出了该类的全部虚函数地址。
- c++是在构造函数中进行虚表的创建和虚表指针的初始化。构造函数的调用顺序:在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针vptr,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针vptr被初始化, 此时 vptr指向自身的虚表。
- 当子类重写了父类的虚函数,则编译器会将子类虚函数表中对应的父类的虚函数替换成子类的函数。如下图所示:
参考:C++对象模型 类对象内存结构
参考:C++ 虚函数表解析
多态调用原理图解
class A
{
public:
int i;
virtual void func() {}
virtual void func2() {}
};
class B : public A
{
int j;
void func() {}
};
假设 pa 的类型是 A*,则 pa->func() 这条语句的执行过程如下:
- 取出 pa 指针所指位置的前 4 个字节,即对象所属的类的虚函数表的地址(在 64 位编译模式下,由于指针占 8 个字节,所以要取出 8 个字节)。如果 pa 指向的是类 A 的对象,则这个地址就是类 A 的虚函数表的地址;如果 pa 指向的是类 B 的对象,则这个地址就是类 B 的虚函数表的地址。
- 根据虚函数表的地址找到虚函数表,在其中查找要调用的虚函数的地址。不妨认为虚函数表是以函数名作为索引来查找的,虽然还有更高效的查找方法。
如果 pa 指向的是类 A 的对象,自然就会在类 A 的虚函数表中查出 A::func 的地址;如果 pa 指向的是类 B 的对象,就会在类 B 的虚函数表中查出 B::func 的地址。
类 B 没有自己的 func2 函数,因此在类 B 的虚函数表中保存的是 A::func2 的地址,这样,即便 pa 指向类 B 的对象,pa->func2();这条语句在执行过程中也能在类 B 的虚函数表中找到 A::func2 的地址。 - 根据找到的虚函数的地址调用虚函数。
菱形继承及虚继承
假设继承体系如左图,则 D 的对象内存布局如右图。
【结论】D的对象可以分成3块,每个直接基类(B和C)各占一块,按继承的顺序排放,最后是派生类(D)自己的成员变量。而B和C的数据块中都拷贝了一份间接基类A的数据。
虚继承的内存分布
图解C++菱形继承、虚继承对象的内存分布
https://blog.csdn.net/qq_41929943/article/details/102641969
class Base
{
public:
int age = 10;
};
class Son1 : virtual public Base
{
};
class Son2 : virtual public Base
{
};
class Grandson : public Son1, public Son2
{
};
int main()
{
Grandson g;
g.age = 40;
cout << "g.age = " << g.age << endl;
g.Son1::age = 20;
g.Son2::age = 30;
cout << "g.Son1::age = " << g.Son1::age << endl;
cout << "g.Son2::age = " << g.Son2::age << endl;
return 0;
}
小数计算机中的表示方法
负数计算机中用补码表示
补码:取反加1,符号位1表示。
空类也有 1Byte 的大小
因为这样才能使得这个 class 的 2 个 objects 在内存中有独一无二的地址
i++/++i
它们会被扩展为三个机器指令:
1,把变量值装入寄存器
2,增加或减少寄存器中的值
3,把寄存器中的值写回内存
volatile(易变的)关键字
volatile的本意是“易变的”。
因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
extern "C"
对于用C++编译器编译的对外暴露的接口,需要使用增加extern "C"
,以编译出C能识别的符号。
#ifdef __cplusplus
extern "C"
{
#endif
要抛出的接口
#ifdef __cplusplus
}
#endif
typedef定义的函数指针
void (*func)(int)
我们分开理解,改表达式为返回值为void,入参为int类型的函数指针。
而void (*signal(int signo, xxx))(int)
,同上,依旧是返回值为void,入参为int类型和xxx
的函数指针。
那么整个void (*signal( int signo, void (*func)(int)) )(int);
首先这个表达式是定义了一个函数,函数名是signal,形参一是int 型,形参二是函数指针,指向的函数,返回值是 void,形参是 int 型。
其实清爽的写法是如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数的作用就是为某个信号注册一个新的信号处理函数,同时返回原来的信号处理函数,以便在退出时进行恢复。
nullptr与NULL的区别
C中定义NULL为 ((void *)0)
。NULL实质上是一个void *
指针。C++中定义的NULL为0。
nullptr关键字用于标识空指针,是std::nullptr_t类型的(constexpr)变量。它可以转换成任何指针类型和bool布尔类型(主要是为了兼容普通指针可以作为条件判断语句的写法),但是不能被转换为整数。
C++中定义NULL和nullptr主要是为了解决了函数重载后的函数匹配问题
'#'、'##'、可变参数
'#':将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号
#define WARN_IF(EXP) \
do { \
if (EXP) \
fprintf(stderr, "Warning: " #EXP "\n"); \
}while(0)
WARN_IF (divider == 0);
WARN_IF (divider == 0);会被解析为如下:
do {
if (divider == 0)
fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0);
'##':为连接符(concatenator),用来将两个Token连接为一个Token
void quit_command()
{
cout<<"quit_command"<<endl;
}
void help_command()
{
cout<<"help_command"<<endl;
}
struct command
{
const char* name;
void (*function) (void);
};
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command commands[] = {
COMMAND(quit),
COMMAND(help)
};
那么如下的代码就会转换为一个二维数组:
struct command commands[] = {
COMMAND(quit),
COMMAND(help)
}
转换为如下:
struct command commands[] = {
"quit", quit_command,
"help", help_command
}
变参宏——##VAR_ARGS
记住使用如下形式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
其中'##'这个连接符号充当的作用就是当VAR_ARGS为空的时候,消除前面的那个逗号(参考:CPP常识 04 -- 宏,#号##号,可变参数)
宏定义打印
#define DVR_PRT(format,...)printf("[File:"__FILE__", Line:%d] "format, __LINE__, ##__VA_ARGS__)
#define PRT(format,...)printf("[File:%s, Line:%d] "format, __FILE__, __LINE__, ##__VA_ARGS__)
打印函数
void DoLog(int level, char* format, ...)
{
char temp[c_maxTempNameLen] = {0};
char tempo[c_maxTempNameLen] = { 0 };
va_list ap;
va_start(ap, format);
SAFE_VSPRINTF(temp, c_maxTempNameLen, format, ap);
va_end(ap);
SAFE_SNPRINTF(tempo, sizeof(tempo), "[HiAndroidLib]%s", temp);
if (logger) {
logger(level, tempo);
} else {
printf("%s\n", tempo);
}
}
宏使用注意事项
1、注意括号括起相关操作
2、使用 #define MY_MACRO(x) do { } while(0);
语句做成与C函数一致的使用体验,即分号体验MY_MACRO(xxx);
3、不要给宏参数传递函数或者函数指针引用,会在宏展开时被多次调用,导致与臆想的不一致。即:MY_MACRO(fun(b))
。
有符号与无符号运算
表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此,从这个意义上讲,无符号数的运算优先级要高于有符号数。
全局变量
定义相同的全局变量在不同的.cpp文件中,链接时会报重定义。都改为static的则不会。
访问私有成员、函数
C++中访问私有成员的方法
1、提供对外的公共接口处理私有成员,比如提供返回指针、引用的公共接口
2、友元类/友元函数
友元函数:friend 类型 函数名(形式参数);
友元类 :friend class 类名;
class A;
class B
{
public:
B()
{
b = 20;
}
friend class A; // A是B的友元,能访问B的私有成员
private:
int b;
};
class A
{
public:
A()
{
a = 10;
}
void print(B &b)
{
cout << a << endl;
cout << b.b << endl;
}
private:
friend void print(A &obj); // 友元函数的声明,放哪都可以
int a;
};
// 友元函数的实现,可以访问A类私有成员
void print(A &obj)
{
cout << obj.a << endl;
}
在main函数前、后执行代码
在main函数前执行代码的方法
1、全局变量构造函数
2、全局变量的赋值函数
3、如果是GNUC的编译器,可在你要执行的方法前加__attribute__((constructor))
__attribute__((constructor)) void func()
{
printf("hello world\n");
}
在main函数后执行代码的方法
1、全局变量析构函数
2、如果是GNUC的编译器,可在你要执行的方法前加__attribute__((destructor))
3、C:onexit(func)
或C++:atexit(func)
this指针
this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。
二维数组内存布局
#include <iostream>
int main()
{
double alpha[][4] = { {0}, {1,2}, {3,4,5} };
std::cout << sizeof(alpha) / sizeof(double);
return 0;
}
void Test4offset(void)
{
int xs[][2] = {0, 1, 2, 3, 4, 5};
int *p1 = &xs[1][0];
printf("%u %u %u %p\n", p1[0], p1[1], *(*(xs + 2) + 1), &xs+1);
}
输出是 2 3 5 0x10018