C++基础知识总结一

代码命名规范

令人头疼的代码命名规范……
神奇的网站https://unbug.github.io/codelf/

1.驼峰命名法(CamelCase)
骆驼式命名法(Camel-Case)又称驼峰式命名法,是电脑程式编写时的一套命名规则(惯例)。正如它的名称CamelCase所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。程序员们为了自己的代码能更容易的在同行之间交流,所以多采取统一的可读性比较好的命名方式。

它又可以分为以下几种。

小驼峰命名法(lowerCamelCase)
除第一个单词之外,其他单词首字母大写。方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。比如:

getUserInfo()
createCustomThreadPool()
findAllByUserName(String userName)

大驼峰命名法(CamelCase)
相比小驼峰法,大驼峰法(即帕斯卡命名法)把第一个单词的首字母也大写了。常用于类名,命名空间等。如:

class TaskDateToSend{}
class TaskLabelToSend{}
SettingRepository

2.蛇形命名法(snake_case)
蛇形法是全由小写字母和下划线组成,在两个单词之间用下滑线连接即可。测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)。如:

first_name
last_name
MAX_ITERATION
LAST_DATA

在这里插入图片描述

源码、反码、补码

计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同 [1] 。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理

1.原码
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:

[+1]= 0000 0001
[-1]= 1000 0001

第一位是符号位. 因为第一位是符号位, 所以8位二进制数的取值范围就是:
[1111 1111 , 0111 1111]
即[-127 , 127]
原码是人脑最容易理解和计算的表示方式.

2.反码
反码的表示方法是:
正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

[+1] = [00000001]= [00000001][-1] = [10000001]= [11111110]

可见如果一个反码表示的是负数, 人脑无法直观的看出来它的数值. 通常要将其转换成原码再计算.
https://blog.csdn.net/zl10086111/article/details/80907428

一个数的反码, 实际上是这个数对于一个膜的同余数. 而这个膜并不是我们的二进制, 而是所能表示的最大值。

3.补码
补码的表示方法是:
正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

[+1] = [00000001]= [00000001]= [00000001][-1] = [10000001]= [11111110]= [11111111]

对于负数, 补码表示方式也是人脑无法直观看出其数值的. 通常也需要转换成原码在计算其数值.

根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了。用补码可以将减法统一成加法,同时可以将符号位参与运算。
例如:

2-1=2+(-1)=[0000 0010]+[1111 1111]=[0000 0001]=1

8位有符号数表示范围[-128~+127]

Unicode、UTF16、UTF8、UTF32

https://blog.csdn.net/WantFlyDaCheng/article/details/103775823

Unicode编码定义了这个世界上几乎所有字符, Unicode用[码点]表示每个字符。U+XXXXXX 是码点的表示形式,X 代表一个十六制数字,可以有 4-6 位,不足 4 位前补 0 补足 4 位,超过则按是几位就是几位。

码点的取值范围
码点的取值范围目前是 U+0000 ~ U+10FFFF,理论大小为 0X10FFFF+1=0X110000(为啥+1,因为从0开始嘛~)。总共有1×16+1=17 个的 65536 的大小,粗略估算为 17×6万=102 万,所以这是一个百万级别的数

UTF16、UTF8、UTF32是unicode的三种编码方式(换句话就是码点如何转换为utf-8或者utf-16或者utf-32)

UTF32
UTF-32 采用的定长四字节编码模式
我们说码点最大的 10FFFF 也就 21 位,而 UTF-32 采用的定长四字节则是 32 位,所以它表示所有的码点不但毫无压力,反而绰绰有余,所以只要把码点的表示形式以前补 0 的形式补够 32 位即可。这种表示的最大缺点是占用空间太大。

UTF16
UTF-16 是一种变长的 2 或 4 字节编码模式
把每 65536 个码点作为一个平面,总共 17 个平面。编号从 0 开始,第一个平面称为 Plane 0。
在这里插入图片描述
第一个平面即是 BMP(Basic Multilingual Plane 基本多语言平面),也叫 Plane 0,它的码点范围是 U+0000 ~ U+FFFF。这也是我们最常用的平面,日常用到的字符绝大多数都落在这个平面内。
UTF-16 只需要用 两字节编码此平面内的字符。

后续的 16 个平面称为 SP(Supplementary Planes)。显然,这些码点已经是超过 U+FFFF 的了,所以已经超过了 16 位空间的理论上限,对于这些平面内的字符, UTF-16 采用了四字节编码。

代理区
你可能还注意到前面的 BMP 缩略图中有一片空白,这白花花一片亮瞎了我们的猿眼的是啥呢?这就是所谓的代理区(Surrogate Area)了。

在这里插入图片描述
可以看到这段空白从 D8~DF。其中前面的红色部分 D800–DBFF 属于高代理区(High Surrogate Area),后面的蓝色部分 DC00–DFFF 属于低代理区(Low Surrogate Area),各自的大小均为 4×256=1024。

UTF-16如何用代理区编码?
UTF-16 是一种变长的 2 或 4 字节编码模式。对于 BMP 内的字符使用 2 字节编码,其它的则使用 4 字节组成所谓的代理对来编码。在前面的鸟瞰图中,我们看到了一片空白的区域,这就是所谓的代理区(Surrogate Area)了,代理区是 UTF-16 为了编码增补平面中的字符而保留的,总共有 2048 个位置,均分为高代理区(D800–DBFF)和低代理区(DC00–DFFF)两部分,各1024,这两个区组成一个二维的表格,共有1024×1024=210×210=24×216=16×65536,所以它恰好可以表示增补的 16 个平面中的所有字符。
在这里插入图片描述
什么是代理对?
一个高代理区(即上图中的Lead(头),行)的加一个低代理区(即上图中的Trail(尾),列)的编码组成一对即是一个代理对(Surrogate Pair),必须是这种先高后低的顺序,如果出现两个高,两个低,或者先低后高,都是非法的。
在图中可以看到一些转换的例子,

如(D800 DC00)—>U+10000,左上角,第一个增补字符
  (DBFF DFFF)—>U+10FFFF,右下角,最后一个增补字符

那UTF-16为何要采用代理对?
个人感觉这里是为了区分二字节和四字节各自表示的字符,每次读取两个字节,遇到 0 X D 800   0 X D F F F 0XD800~0XDFFF 0XD800 0XDFFF之间数,就提示你应该连续读取四个字节,用这四个字节表示的编码去找对应的字符;而如果不是 0 X D 800   0 X D F F F 0XD800~0XDFFF 0XD800 0XDFFF之间数,则提示你应该用读取到的两个字节表示的编码去找对应的字符。
UTF-16相当于牺牲了高代理区(D800–DBFF)和低代理区(DC00–DFFF)两部分空间,但是确新增了 1024 ∗ 1024 = 16 ∗ 65536 1024*1024=16*65536 10241024=1665536的空间。依次来实现了扩容!

码点到 UTF-16 如何转换?
转换分成两部分:
1 BMP 中直接对应,无须做任何转换,也就是如果U<0x10000,U的UTF-16编码就是U对应的16位无符号整数;

2.增补平面 SP 中,则需要做相应的计算。也就是如果U≥0x10000的情况
我们先计算U’=U-0x10000,然后将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,转换为16进制即0xD843 0xDC30。

UTF8
UTF-8 是变长的编码方案,可以有 1,2,3,4 四种字节组合。UTF-8 采用了高位保留方式来区别不同变长,如下:
(得保证解码无歧义,搜索匹配无多个匹配)
在这里插入图片描述
可以看到,由于最高位不同,多字节中不会包含一字节的模式。对于 UTF-8 而言,二字节的模式也不会包含在三字节模式中,也不会在四字节中;三字节模式也不会在四字节模式中,这样就解决上面所说的搜索匹配难题。

UTF-8如何与码点进行转换
在这里插入图片描述
对于Unicode的编码首先确定它的范围,找到它是对应的几字节。
对于0x00-0x7F之间的字符,UTF-8编码与[ASCII编码]完全相同。

“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

C/C++中字面量数的类型是int

https://stackoverflow.com/questions/61624859/why-does-long-long-2147483647-1-2147483648

long long a, b;
a = 2147483647 + 1;
b = 2147483648;
printf("%lld\n", a);
printf("%lld\n", b);

这段代码最后的输出
在这里插入图片描述
为啥两个不一样呢?
因为字面的常量被认为是int,2147483647 + 1被认为是两个int相加,溢出了;
而2147483648大于int的最大值,所以编译器自己把它认为成long或long long因此没有溢出。
可以改成

long long a, b;
a = 2147483647u + 1;//or a= 2147483647LL + 1;
b = 2147483648;
printf("%lld\n", a);
printf("%lld\n", b);

C语言中无符号数和有符号数之间的运算

https://www.cnblogs.com/qingergege/p/7507533.html
C语言中有符号数和无符号数进行运算(包括逻辑运算和算术运算)默认会将有符号数看成无符号数进行运算,其中算术运算默认返回无符号数,逻辑运算当然是返回0或1了。

    int a=-2;
    unsigned int b=1;
    cout<<a+b<<endl;
    cout<<0xffffffff<<endl;

在这里插入图片描述

负数的左移和右移

在机器中,数的二进制码都是其补码。
① 负数的右移:需要保持数为负数,所以操作是对负数的二进制位左边补1。如果一直右移,最终会变成-1,即(-1)>>1是-1。

② 负数的左移:和整数左移一样,在负数的二进制位右边补0,一个数在左移的过程中会有正有负的情况,所以切记负数左移不会特殊处理符号位。如果一直左移,最终会变成0。

计算机中浮点数的表示、存储方式

float,double类型的存储方式和精度丢失

  1. 根据IEEE 754浮点数计数标准,浮点数可以表示 1. M . . . ∗ 2 E 1.M...*2^E 1.M...2E采用尾数M+阶码E的编码方式,
    因此,只要给出符号(S)、阶码(E)、尾数(M),这三个信息就能完全表示一个浮点数,
  • 单精度浮点数float(32位,4字节):
    在这里插入图片描述
    双精度浮点数double(64位,8字节):
    在这里插入图片描述
  1. 以单精度浮点型(float)为例:
  • Sign(1bit): 符号位。表示浮点数是正数还是负数。0表示正数,1表示负数
  • Exponent(8bits):指数部分。对于float来说,这里的8位二进制可以表示256种状态,不过为了表示方便,浮点型的指数位都有一个固定的偏移量(bias),用于使指数+这个偏移量 = 一个非负整数,这样就不用担心如何表示负数了,规定:在32位单精度类型中,这个偏移量是 127 = 2 7 − 1 127=2^7-1 127=271。在64位双精度类型中,偏移量是 1023 = 2 10 − 1 1023=2^{10}-1 1023=2101.
    所以, IEEE754规定,指数位用于表示【-126,127】范围内的指数(理论上的范围【-127,128】,但阶数不包括全零和全一的情况因此它的取值范围是【-126,127】64位双精度类型中阶数的取值范围是【-1022,+1023】

关于这里阶数为什么不包含全零和全一的情况,主要是为了保留用作特殊值的处理,NaN、 ±0,±∞、以及非规范化数。

对于单精度浮点数,所有这些特殊值都由保留的特殊指数值 -127 和 128 来编码。如果我们分别用 emin 和 emax 来表达其它常规指数值范围的边界,即 -126 和 127,则保留的特殊指数值可以分别表达为 emin - 1 和 emax + 1;
详细参考https://blog.csdn.net/hyforthy/article/details/19649969
在这里插入图片描述

对于单精度浮点数,举例:
如果运算后得到的指数是-126,则加上偏移量127后,这里的8bit空间填写1;
如果运算后得到的指数是-10,则加上偏移量127后,这里的8bit空间填写117;
看得出来,有了偏移量,指数位中始终是一个非负整数。

  • fraction(23bits):基数部分。浮点数具体数值的实际表示。

举例:对于一个十进制数20.75,其在float类型变量中的存储
第一步:将十进制化为二进制(整数部分:除k取余法,结果逆序排列;小数部分:乘k取整法,结果正序排列)
在这里插入图片描述
可知:十进制数20.75,表示成二进制数是10100.11。
第二步,将其转换成阶码+尾数表示 1. M . . . ∗ 2 E 1.M...*2^E 1.M...2E
10100.11 = 1.010011 ∗ 2 4 10100.11=1.010011*2^4 10100.11=1.01001124
第三步,确定符号位,指数部分,尾数部分的数值,
Sign(1bit): 0
指数部分(8bits):127+4=131=1000 0011
尾数部分(23bits):010011=0100 1100 0000 0000 0000 000(黄色部分为补零位)
所以float存储格式位
0 1000 0011 0100 1100 0000 0000 0000 000

可以在线验证:
http://www.binaryconvert.com/result_float.html?decimal=050048046055053
在这里插入图片描述

运算符的优先级

C++运算符优先级表,从上到下,从左到右,优先级依次减弱。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
大致口诀“单目乘除为加减,移位比较位逻辑”
逻辑运算符的优先级是 !大于 && 大于||,但是注意结合性是从左往右
关于优先级和结合性的问题:这里有一个题

1.	#include<iostream>
2.	using namespace std;
3.	int main() {
4.		int x = 3;
5.		int y = 1 || (x = 1) && (x += 1);
6.		printf("x = %d , y = %d", x, y);
7.		return 0;
8.	}

这段代码输出啥?
记住,&&优先级高,所以上面等价于int y = 1 || ((x = 1) && (x += 1));
对后面加了括号,但是,整体的结合性是从左往右。
所以先1,就不运行后面的了,
最终y=1,x还是3

C++基本数据类型

https://www.runoob.com/cplusplus/cpp-data-types.html
C++11语言有一组基本类型,对应于计算机的基本存储单元和使用这些单元去保存数据的一些常用方式 ,
在这里插入图片描述
注意下面的long在32位处理器和64位处理器上的位数不同
参考:32位和64位系统下 int、char、long、double所占的内存
https://blog.csdn.net/single6/article/details/82623357
在这里插入图片描述

#include<iostream>  
#include <limits>

using namespace std;
int main()
{
    cout << "type: \t\t" << "************size**************" << endl;
    cout << "bool: \t\t" << "所占字节数:" << sizeof(bool);
    cout << "\t最大值:" << (numeric_limits<bool>::max)();
    cout << "\t\t最小值:" << (numeric_limits<bool>::min)() << endl;
    cout << "char: \t\t" << "所占字节数:" << sizeof(char);
    cout << "\t最大值:" << (numeric_limits<char>::max)();
    cout << "\t\t最小值:" << (numeric_limits<char>::min)() << endl;
    cout << "signed char: \t" << "所占字节数:" << sizeof(signed char);
    cout << "\t最大值:" << (numeric_limits<signed char>::max)();
    cout << "\t\t最小值:" << (numeric_limits<signed char>::min)() << endl;
    cout << "unsigned char: \t" << "所占字节数:" << sizeof(unsigned char);
    cout << "\t最大值:" << (numeric_limits<unsigned char>::max)();
    cout << "\t\t最小值:" << (numeric_limits<unsigned char>::min)() << endl;
    cout << "wchar_t: \t" << "所占字节数:" << sizeof(wchar_t);
    cout << "\t最大值:" << (numeric_limits<wchar_t>::max)();
    cout << "\t\t最小值:" << (numeric_limits<wchar_t>::min)() << endl;
    cout << "short: \t\t" << "所占字节数:" << sizeof(short);
    cout << "\t最大值:" << (numeric_limits<short>::max)();
    cout << "\t\t最小值:" << (numeric_limits<short>::min)() << endl;
    cout << "int: \t\t" << "所占字节数:" << sizeof(int);
    cout << "\t最大值:" << (numeric_limits<int>::max)();
    cout << "\t最小值:" << (numeric_limits<int>::min)() << endl;
    cout << "unsigned: \t" << "所占字节数:" << sizeof(unsigned);
    cout << "\t最大值:" << (numeric_limits<unsigned>::max)();
    cout << "\t最小值:" << (numeric_limits<unsigned>::min)() << endl;
    cout << "long: \t\t" << "所占字节数:" << sizeof(long);
    cout << "\t最大值:" << (numeric_limits<long>::max)();
    cout << "\t最小值:" << (numeric_limits<long>::min)() << endl;
    cout << "unsigned long: \t" << "所占字节数:" << sizeof(unsigned long);
    cout << "\t最大值:" << (numeric_limits<unsigned long>::max)();
    cout << "\t最小值:" << (numeric_limits<unsigned long>::min)() << endl;
    cout << "long long: \t" << "所占字节数:" << sizeof(long long);
    cout << "\t最大值:" << (numeric_limits<long long>::max)();
    cout << "\t最小值:" << (numeric_limits<long long>::min)() << endl;

    cout << "double: \t" << "所占字节数:" << sizeof(double);
    cout << "\t最大值:" << (numeric_limits<double>::max)();
    cout << "\t最小值:" << (numeric_limits<double>::min)() << endl;
    cout << "long double: \t" << "所占字节数:" << sizeof(long double);
    cout << "\t最大值:" << (numeric_limits<long double>::max)();
    cout << "\t最小值:" << (numeric_limits<long double>::min)() << endl;
    cout << "float: \t\t" << "所占字节数:" << sizeof(float);
    cout << "\t最大值:" << (numeric_limits<float>::max)();
    cout << "\t最小值:" << (numeric_limits<float>::min)() << endl;
    cout << "size_t: \t" << "所占字节数:" << sizeof(size_t);
    cout << "\t最大值:" << (numeric_limits<size_t>::max)();
    cout << "\t最小值:" << (numeric_limits<size_t>::min)() << endl;
    cout << "string: \t" << "所占字节数:" << sizeof(string) << endl;
    // << "\t最大值:" << (numeric_limits<string>::max)() << "\t最小值:" << (numeric_limits<string>::min)() << endl;  
    cout << "type: \t\t" << "************size**************" << endl;
    return 0;
}

静态库与动态库

什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)

二者的不同点在于代码被载入的时刻不同。静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大。动态库(共享库)的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该动态库(共享库)的实例。

静态库和动态库的最大区别,静态情况下,把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度,和降低程序的耦合度。

静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在

回顾一下,将一个程序编译成可执行程序的步骤:
在这里插入图片描述
一般创建静态库的步骤如图所示:
在这里插入图片描述
动态库
通过上面的介绍发现静态库,容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢?

为什么需要动态库,其实也是静态库的特点导致。

  • 空间浪费是静态库的一个问题。
    在这里插入图片描述
  • 另一个问题是静态库对程序的更新、部署和发布也会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
    动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
    在这里插入图片描述
    动态库特点总结:
  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单。
  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。

静态链接和动态链接区别

静态链接和动态链接区别
静态链接
由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件

以下面这个图来简单说明一下从静态链接到可执行文件的过程,根据在源文件中包含的头文件和程序中使用到的库函数,如stdio.h中定义的printf()函数,在libc.a中找到目标文件printf.o(这里暂且不考虑printf()函数的依赖关系),然后将这个目标文件和我们hello.o这个文件进行链接形成我们的可执行文件。
在这里插入图片描述
这里有一个细节,就是从上面的图中可以看到静态运行库里面的一个目标文件只包含一个函数,如libc.a里面的printf.o只有printf()函数,vfprintf.o里面只有vfprintf函数。

我们知道,链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来,如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了输出结果中。由于运行库有成百上千个函数,数量非常庞大,每个函数独立地放在一个目标文件中可以尽量减少空间的浪费,那些没有被用到的目标文件就不要链接到最终的输出文件中。

静态链接的优缺点
静态链接的缺点很明显,
一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本
另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快

动态链接
1.为什么会出现动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。

2.动态链接的原理
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。下面简单介绍动态链接的过程:

假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,即program1.o依赖于lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序

3.动态链接的优缺点
动态链接的优点

一:即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;

二:更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

动态链接的缺点
因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

据估算,动态链接和静态链接相比,性能损失大约在5%以下。

经过实践证明,这点性能损失用来换区程序在空间上的节省和程序构建和升级时的灵活性是值得的。

4.动态链接地址是如何重定位的呢?
前面我们讲过静态链接时地址的重定位,那我们现在就在想动态链接的地址又是如何重定位的呢?
虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行

动态绑定与静态绑定

C++在面向对象编程中,存在着静态绑定和动态绑定的定义,本节即是主要讲述这两点区分。我是在一个类的继承体系中分析的,因此下面所说的对象一般就是指一个类的实例。

首先我们需要明确几个名词定义:
• 静态类型:对象在声明时采用的类型,在编译期既已确定;
• 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
• 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
• 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

先看代码和运行结果:

class A
{
public:
    /*virtual*/ void func(){ std::cout << "A::func()\n"; }
};
class B : public A
{
public:
    void func(){ std::cout << "B::func()\n"; }
};
class C : public A
{
public:
    void func(){ std::cout << "C::func()\n"; }
};

下面逐步分析测试代码及结果,

C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*; 
B* pb = new B(); //pb的静态类型和动态类型也都是B*; 
A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*; 
pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*; 
C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
如果明白上面代码的意思,请继续
pa->func();//A::func()pa的静态类型永远都是A*,不管其指向哪个子类,都直接调用A::func(); 
pc->func(); //C::func() pc的动、静态类型都是C*,因此调用C::func(); 
pnull->func(); //C::func()不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;

如果注释掉类C中的func函数定义,其他不变,即

class C : public A 
{  }; 
pa->func(); //A::func() 理由同上; 
pc->func(); //A::func() pc在类C中找不到func的定义,因此到其基类中寻找;
pnull->func(); //A::func(),在类C中找不到func的定义,因此到其基类中寻找;
如果为A中的void func()函数添加virtual特性,其他不变,即
class A 
 { 
 public: 
virtual void func(){ std::cout << "A::func()\n"; } 
 };
pa->func(); //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用; 
pc->func(); //C::func() pc的动、静态类型都是C*,因此也是先在C中查找; 
pnull->func(); //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;

分析:
在上面的例子中,

  1. 如果基类A中的func不是virtual函数,那么不论pa、pb、pc指向哪个子类对象,对func的调用都是在定义pa、pb、pc时的静态类型决定,早已在编译期确定了。
    同样的空指针也能够直接调用no-virtual函数而不报错(这也说明一定要做空指针检查啊!),因此静态绑定不能实现多态;
  2. 如果func是虚函数,那所有的调用都要等到运行时根据其指向对象的类型才能确定,比起静态绑定自然是要有性能损失的,但是却能实现多态特性;
    本文代码里都是针对指针的情况来分析的,但是对于引用的情况同样适用。
    至此总结一下静态绑定和动态绑定的区别:
  3. 静态绑定发生在编译期,动态绑定发生在运行期;
  4. 对象的动态类型可以更改,但是静态类型无法更改;
  5. 要想实现动态,必须使用动态绑定;
  6. 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

建议:
绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG;

另外,在动态绑定也即在virtual函数中,要注意默认参数的使用。当缺省参数和virtual函数一起使用的时候一定要谨慎,不然出了问题怕是很难排查。
看下面的代码:

class E
 {
  public:
      virtual void func(int i = 0)
      { 
          std::cout << "E::func()\t"<< i <<"\n";
      }
  };
  class F : public E
{
 public:
     virtual void func(int i = 1)
     {
         std::cout << "F::func()\t" << i <<"\n";
     }
};

void test2()
{
     F* pf = new F();
     E* pe = pf;
     pf->func(); //F::func() 1  正常,就该如此;
     pe->func(); //F::func() 0  哇哦,这是什么情况,调用了子类的函数,却使用了基类中参数的默认值!
}

为什么会有这种情况,请看《Effective C++ 第三版》 条款37。
这里只给出建议:
绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。

软链接/硬链接

Linux硬链接和软链接详解(深度剖析)

首先,从使用的角度讲,两者没有任何区别,都与正常的文件访问方式一样,支持读写,如果是可执行文件的话也可以直接执行。
那区别在哪呢?在底层的原理上。

【硬连接】
硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。 其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。
在这里插入图片描述
在 inode 信息中,是不会记录文件名称的,而是把文件名记录在上级目录的 block 中。也就是说,目录的 block 中记录的是这个目录下所有一级子文件和子目录的文件名及 inode 的对应;而文件的 block 中记录的才是文件实际的数据。

当我们查找一个文件,比如 /root/test 时,要经过以下步骤:

  1. 首先找到根目录的 inode(根目录的 inode 是系统已知的,inode 号是 2),然后判断用户是否有权限访问根目录的 block。
  2. 如果有权限,则可以在根目录的 block 中访问到 /root 的文件名及对应的 inode 号。
  3. 通过 /root/ 目录的 inode 号,可以查找到 /root/ 目录的 inode 信息,接着判断用户是否有权限访问 /root/ 目录的 block。
  4. 如果有权限,则可以从 /root/ 目录的 block 中读取到 test 文件的文件名及对应的 inode 号。
  5. 通过 test 文件的 inode 号,就可以找到 test 文件的 inode 信息,接着判断用户是否有权限访问 test 文件的 block。
  6. 如果有权限,则可以读取 block 中的数据,这样就完成了 /root/test 文件的读取与访问。

按照这个步骤,在给源文件 /root/test 建立了硬链接文件 /tmp/test-hard 之后,在 /root/ 目录和 /tmp/ 目录的 block 中就会建立 test 和 test-hard 的信息,这个信息主要就是文件名和对应的 inode 号。但是我们会发现 test 和 test-hard 的 inode 信息居然是一样的,那么,我们无论访问哪个文件,最终都会访问 inode 号是 262147 的文件信息。

这就是硬链接的原理。硬链接的特点如下:

  • 不论是修改源文件(test 文件),还是修改硬链接文件(test-hard 文件),另一个文件中的数据都会发生改变。
  • 不论是删除源文件,还是删除硬链接文件,只要还有一个文件存在,这个文件(inode 号是 262147 的文件)都可以被访问。
  • 硬链接不会建立新的 inode 信息,也不会更改 inode 的总数。
  • 硬链接不能跨文件系统(分区)建立,因为在不同的文件系统中,inode 号是重新计算的。
  • 硬链接不能链接目录,因为如果给目录建立硬链接,那么不仅目录本身需要重新建立,目录下所有的子文件,包括子目录中的所有子文件都需要建立硬链接,这对当前的 Linux 来讲过于复杂。

硬链接的限制比较多,既不能跨文件系统,也不能链接目录,而且源文件和硬链接文件之间除 inode 号是一样的之外,没有其他明显的特征。这些特征都使得硬链接并不常用,大家有所了解就好。

【软连接】
另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息

在这里插入图片描述
软链接和硬链接在原理上最主要的不同在于:硬链接不会建立自己的 inode 索引和 block(数据块),而是直接指向源文件的 inode 信息和 block,所以硬链接和源文件的 inode 号是一致的;而软链接会真正建立自己的 inode 索引和 block,所以软链接和源文件的 inode 号是不一致的,而且在软链接的 block 中,写的不是真正的数据,而仅仅是源文件的文件名及 inode 号。

我们来看看访问软链接的步骤和访问硬链接的步骤有什么不同。

  1. 首先找到根目录的 inode 索引信息,然后判断用户是否有权限访问根目录的 block。
  2. 如果有权限访问根目录的 block,就会在 block 中查找到 /tmp/ 目录的 inode 号。
  3. 接着访问 /tmp/ 目录的 inode 信息,判断用户是否有权限访问 /tmp/ 目录的 block。
  4. 如果有权限,就会在 block 中读取到软链接文件 check-soft 的 inode 号。因为软链接文件会真正建立自己的 inode 索引和 block,所以软链接文件和源文件的 inode 号是不一样的。
  5. 通过软链接文件的 inode 号,找到了 check-soft 文件 inode 信息,判断用户是否有权限访问 block。
  6. 如果有权限,就会发现 check-soft 文件的 block 中没有实际数据,仅有源文件 check 的 inode 号。
  7. 接着通过源文件的 inode 号,访问到源文件 check 的 inode 信息,判断用户是否有权限访问 block。
  8. 如果有权限,就会在 check 文件的 block 中读取到真正的数据,从而完成数据访问。

通过这个过程,我们就可以总结出软链接的特点(软链接的特点和 Windows 中的快捷方式完全一致)。

  • 不论是修改源文件(check),还是修改硬链接文件(check-soft),另一个文件中的数据都会发生改变。
  • 删除软链接文件,源文件不受影响。而删除原文件,软链接文件将找不到实际的数据,从而显示文件不存在。
  • 软链接会新建自己的 inode 信息和 block,只是在 block 中不存储实际文件数据,而存储的是源文件的文件名及 inode 号。
  • 软链接可以链接目录。
  • 软链接可以跨分区。

C++函数参数入栈顺序

C++参数的传输顺序是从右到左的

但这里还有个问题,为什么采用从右到左的参数方式,而不使用从左到右的传参方式呢?
是的,这个问题很好,其实主要原因就在于支持可变长参数,而C语言支持这种特色,正是这个原因使得C语言函数参数入栈顺序为从右至左。
具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

补充:
函数调用栈 剖析+图解
当发生函数调用的时候,栈空间中存放的数据是这样的:
1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;
所以,发生函数调用时,入栈的顺序为:

参数N
参数N-1
参数N-2

参数3
参数2
参数1
函数返回地址
上一层调用函数的EBP/BP
局部变量1
局部变量2

局部变量N

函数调用栈如下图所示:
在这里插入图片描述

C++地址空间从上到下讲讲

Linux虚拟地址空间布局
在这里插入图片描述
在这里插入图片描述
在32位X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。0x08048000以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。通过使用mmap系统调用,可访问0x08048000以下的地址空间。

通过cat /proc/self/maps命令查看加载表如下:
在这里插入图片描述
下面以 C++ 为例,看一下常见变量所属的内存段

来自https://blog.csdn.net/K346K346/article/details/45592329

#include <string.h>
int a = 0;                 // a在数据段,0为文字常量,在代码段
char *p1;                // BSS段,系统默认初始化为NULL
void main()
{
    int b;                 					//栈
    char *p2 = "123456";  			//字符串"123456"在代码段,p2在栈上
    static int c =0;      			//c在数据段
    const int d=0; 					//栈
    static const int d;				//数据段
    p1 = (char*)malloc(10);		   //分配的10字节在堆
    strcpy(p1,"123456"); 			//"123456"放在代码段,编译器可能会将它与p2所指向的"123456"优化成一个地方
}

C++关键字

static关键字

分三类进行理解static:
修饰局部变量/修饰全局变量;static修饰函数;C++类中的static

static的作用
1)隐藏
程序一般有多个源文件,函数如果加了static,就会对其它源文件隐藏,就是说这个函数只能在定义它的源文件中被使用。

Static+局部/全局变量,
第一个作用,也是隐藏,static +全局变量会使得全局变量只在定义它的源文件中被使用,static+局部变量,只能在函数内部使用。
第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
第三个作用是默认初始化为0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加’\0’太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是’\0’。

下面是中兴通讯2012校招笔试题的一道问答题:

1.static全局变量与普通的全局变量有什么区别 ?

全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。
全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者都存在与静态存储数据区。

  • 这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
  • static全局变量只初使化一次,防止在其他文件单元中被引用;
  1. static局部变量和普通局部变量有什么区别 ?

把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。
static局部变量只被初始化一次,下一次依据上一次结果值;

  1. static函数与普通函数有什么区别?

static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static修饰的函数),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件.

C++类中的static
如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;
如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用。

volatile关键字

https://zhuanlan.zhihu.com/p/33074506
volatile关键字解析

前提:C 和 C++ 语言中的 volatile 关键字与Java 等语言中的 volatile 关键字不完全相同,下面只讨论C/C++中的 volatile 关键字。

Volatile关键词的第一个特性:易变性。所谓的易变性,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。

Volatile关键词的第二个特性:“不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。

volatile关键词的第三个特性:单线程执行的前提下,”顺序性”,能够保证volatile变量间的顺序性,编译器不会进行对volatile修饰的语句进行乱序优化,但是,注意没有用volatile和用了volatile的语句还是可能被乱序优化。

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化(一般的变量,编译器会对变量值进行优化,将内存中的值放在寄存器中,加快读写效率),系统总是重新从它所在的内存读取数据
volatile int i=10;

一般说来,volatile用在如下的几个地方:
• 1) 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
• 2) 多任务环境下各任务间共享的标志应该加 volatile;
• 3) 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

volatile 指针
和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:
修饰由指针指向的对象、数据是 const 或 volatile 的:
const char* cpch;
volatile char* vpch;

指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:
char* const pchc;
char* volatile pchv;

多线程下的volatile
注意「volatile 不能用作同步」,不能用 volatile 修饰 while 的退出 flag来同步参考:

const关键字

分几个方面理解:
const全局/局部变量;
const 修饰指针变量;
const修饰函数参数;
const修饰函数返回值;
const修饰类的数据成员;
const修饰类成员函数;
const修饰类对象,定义常量对象 ;

const全局/局部变量
const全局常量存储空间在文本常量区data段(只读)上分配,而const局部常量其存储空间在栈区stack段(可读可写)分配。
const+全局变量:C++中,const限定符把一个对象转换成一个常量:const int bufSize = 512;因为常量在定义后就不能被修改,所以定义时必须初始化。在全局作用域声明const变量是定义该对象的文件的局部变量;这个变量只能在所在的文件中被访问,不能被其他文件访问。通过指定extern const就可以在整个程序中访问const对象(const 默认是带static属性的,这里使用extern, 相当于将const的static属性给去掉了)。
举例:
https://blog.csdn.net/liuhhaiffeng/article/details/82623785?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-2

用法一:仅在此文件下被访问的const变量
// demo.h

const int MyAge = 30;//属于定义

// test1.cpp

#include "demo.h"
void test1()
{
    std::cout << MyAge;
}

// test2.cpp

#include "demo.h"
void test2()
{
  std::cout << MyAge;
}

在头文件直接定义const变量, 那么当此头文件被多个地方引用时, 相当于每个引用的地方都包含了此const变量的一个副本, 即: 从存储空间上讲, 如果有3处引用, 就相当于定义3个相同的”局部const变量”

用法二:全局被访问的const变量
// demo.h

// 仅仅是声明式
extern const int MyAge;//属于声明

// demo.cpp

// 这里才是定义式
const int MyAge = 30;

// test1.cpp

#include "demo.h"
void test1()
{
    std::cout << MyAge;
}

// test2.cpp

#include "demo.h"
void test2()
{
  std::cout << MyAge;
}

如果此头文件被多个地方引用时, 此const变量始终只有一个, 可以说是真正的全局变量

三:以下是错误的,
注意: 这里虽然加了extern, 但在定义声明的同时, 又给MyAge初始化了, 所以
严格的说: demo.h中extern const int MyAge = 30;是定义式。如果demo.h被多个.cpp包含, 就会出现"重复定义错误"
//demo.h

extern const int MyAge = 30;//一般在定义的时候初始化,这里在声明时初始化,肯定错了呀

// test1.cpp

#include "demo.h"
void test1()
{
    std::cout << MyAge;
}

// test2.cpp

#include "demo.h"
void test2()
{
  std::cout << MyAge;
}

const局部变量
const全局常量才是真正意上的常量,而局部const常量其不是真正意义上的常量。
const全局常量存储空间在文本常量区data段(只读)上分配,而const局部常量其存储空间在栈区stack段(可读可写)分配。

#include <iostream>
using std::cout;
using std::endl;
const int i = 10; //文本常量区data段(只读)
int main()
{
int *pi = (int *) &i;
cout << *pi << endl;
cout << i << endl;
*pi = 100;
cout << *pi << endl;
cout << i << endl;
return 0;
}
 g++下输出结果:
10
10
段错误

2:

#include <iostream>
using std::cout;
using std::endl;
int main()
{
const int i = 10;// 栈区(可读可写)
int *pi = (int *) &i;
cout << *pi << endl;
cout << i << endl;
*pi = 100;
cout << *pi << endl;
cout << i << endl;
return 0;
}
g++下输出结果:
10
10
100
10

解释:
在C++中对于基本类型的常量,编绎器并不为其分配存储空间,编译器会把它放到符号表,当取符号常量的地址等操作时,将强迫编译器为这些常量分配存储空间,通过地址访问修改的是这个拷贝的副本而非原始的符号常量。

const 修饰指针变量
const 修饰指针变量有以下三种情况。

A: const 修饰指针指向的内容,则内容为不可变量。 
B: const 修饰指针,则指针为不可变量。 
C: const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
const int *p = 8;// 指针指向的内容为不可变量
int a = 8;
int* const p = &a;// 指针为不可变量
int a = 8; 
const int * const p = &a;// p 指向的内容和指向的内存地址都已固定,不可改变。

const修饰函数参数
const修饰参数是为了防止函数体内可能会修改参数原始对象。因此,有三种情况可讨论:
函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const。
函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针指向被篡改,加上底层const可以防止指向对象被篡改。
函数参数为引用:引用传递(pass-by-reference)有一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原始对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层const,既可以减小拷贝开销,又可以防止修改底层所引用的对象。

const修饰函数返回值
令函数返回一个常量,可以有效防止因用户错误造成的意外。
Const 修饰返回值分三种情况。
A:const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
B: const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
C: const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。

const修饰类的数据成员
const数据成员的初始化只能在类构造函数的初始化表中进行

const修饰类成员函数
A fun4()const; 其意义上是不能修改所在类的的任何变量,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。
注意:const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。
如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用 mutable 关键字修饰这个成员,mutable 的意思也是易变的,容易改变的意思,被 mutable 关键字修饰的成员可以处于不断变化中,如下面的例子。

#include<iostream> 
using namespace std; 
class Test { 
public: 
Test(int _m,int _t):_cm(_m),_ct(_t)
{} 
void Kf()const { 
++_cm; // 错误 
++_ct; // 正确 
} 
private: 
int _cm; 
mutable int _ct; 
}; 
int main(void) { 
Test t(8,7); 
return 0; 
}

const修饰类对象,定义常量对象
常量对象只能调用常量函数,别的成员函数都不能调用。

mutable关键字

mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。

知乎回答
在这里插入图片描述
在这里插入图片描述

extern

在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。

在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。

explicit

C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。

C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。
1 是个构造;2 是个默认且隐含的类型转换操作符。
所以, 有时候在我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA的对象。

这样看起来好象很酷, 很方便。 但在某些情况下, 却违背了程序员的本意。 这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用/使用, 不能作为类型转换操作符被隐含的使用。

解析:explicit构造函数是用来防止隐式转换的。请看下面的代码:

#include <iostream>
using namespace std;
class Test1
{
public :
	Test1(int num):n(num){}
private:
	int n;
};
class Test2
{
public :
	explicit Test2(int num):n(num){}
private:
	int n;
};
 
int main()
{
	Test1 t1 = 12;
	Test2 t2(13);
	Test2 t3 = 14;
		
	return 0;
}

编译时,会指出 t3那一行error:无法从“int”转换为“Test2”。而t1却编译通过。注释掉t3那行,调试时,t1已被赋值成功。

注意:当类的声明和定义分别在两个文件中时,explicit只能写在在声明中,不能写在定义中。

宏展开和内联函数区别

宏定义不检查函数参数,返回值什么的,只是展开
内联函数会检查参数类型,所以更安全。
任何在类的说明部分定义的函数都会被自动的认为是内联函数。

声明内联函数看上去和普通函数非常相似:

  void f(int i, char c); 

当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件:

  inline 
  void f(int i, char c) 
  { 
      // ... 
  }

当函数调用的时候发生了什么?

例如:

int main(void)
{
    foo(1,2,3) ;
    return 0 ;
}

在这里插入图片描述
方法main需要调用foo时,它的标准行为:
1、在main方法的调用栈中,将 foo的参数从右向左 依次push到栈中。

2、把main方法当前指令的 下一条指令地址 (即return address)push到栈中。(隐藏在call指令中)

3、使用call指令调用目标函数体foo。

请注意,以上3步都处于main的调用栈,其中ebp保存其栈底,而esp保存其栈顶。
接下来,在foo函数中:
1、push ebp: 将ebp的当前值push到栈中,即保存ebp。

2、mov ebp,esp: 将esp的值赋给ebp,则意味着进入了foo方法的调用栈。

3、[可选]sub esp, XXX: 在栈上分配XXX字节的临时空间。(抬高栈顶)(编译器根据函数中的局部变量的总大小确定临时空间的大小)

4、[可选]push XXX: 保存(push)一些寄存器的值。

而在foo方法调用完毕后,便执行前面阶段的逆操作:
1、保存返回值: 通常将函数的返回值保存在寄存器eax中。

2、[可选]恢复(pop)一些寄存器的值。

3、mov esp,ebp: 恢复esp同时回收局部变量空间。(恢复原栈顶)

4、pop ebp: 将栈顶的值赋给ebp,即恢复main调用栈的栈底。(恢复原栈底)

5、ret:从栈顶获得之前保留的return address,并跳转到此位置继续执行。

栈与堆的区别

https://blog.csdn.net/qq_35987777/article/details/102975959
栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  1. 和堆一样存储在计算机 RAM 中。
  2. 在栈上创建变量的时候会扩展,并且会自动回收。
  3. 相比堆而言在栈上分配要快的多。
  4. 用数据结构中的栈实现。
  5. 存储局部数据,返回地址,用做参数传递。
  6. 当用栈过多时可导致栈溢出(无穷次(大量的)的递归调用,或者大量的内存分配)。
  7. 在栈上的数据可以直接访问(不是非要使用指针访问)。
  8. 如果你在编译之前精确的知道你需要分配数据的大小并且不是太大的时候,可以使用栈。
  9. 当你程序启动时决定栈的容量上限。

堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

  1. 和栈一样存储在计算机RAM。
  2. 在堆上的变量必须要手动释放,不存在作用域的问题。数据可用 delete, delete[] 或者 free 来释放。
  3. 相比在栈上分配内存要慢。
  4. 通过程序按需分配。
  5. 大量的分配和释放可造成内存碎片。
  6. 在 C++ 中,在堆上创建数的据使用指针访问,用 new 或者 malloc 分配内存。
  7. 如果申请的缓冲区过大的话,可能申请失败。
  8. 在运行期间你不知道会需要多大的数据或者你需要分配大量的内存的时候,建议你使用堆。
  9. 可能造成内存泄露。

栈和堆都是用来从底层操作系统中获取内存的。
在多线程环境下每一个线程都可以有他自己完全的独立的栈,但是他们共享堆

new一个类或结构体的用法

new 一个类或者结构体的话,实际上就是调用了他的无参数构造函度数。
不加括号或者加上空括号,指名了调用类的无参数的构造函数 ;括问号内加参数的话,就是调用其他带参数的类的构造函数。
总结:不带括号的比较简洁,但是有一定局属限性,无法带参数实例化一个类。

typedef struct {
    unsigned int id;  
}field;

field *res=new field;//等价于Field *res=new field();都是调用其无参构造函数,

C++中结构体和类的唯一区别就是默认访问权限不同,所以new 一个对象同上类,默认private,结构体默认public

C++中struct和class的区别

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承(《C++继承与派生》一章会讲解继承)。
  • class 可以使用模板,而 struct 不能(《模板、字符串和异常》一章会讲解模板)。

C++模板之typename和class关键字的区别

在模板类的声明中,我们有两种方式:

template <class T>
template <typename T>

一般情况下,这两种方式是相同的,
但是当T 是一个类,而这个类又有子类(假设名为 innerClass) 时,应该用 template
typename T::innerClass myInnerObject; //这里的 typename 告诉编译器,T::innerClass 是一个类,程序要声明一个 T::innerClass 类的对象,而不是声明 T 的静态成员,而 typename 如果换成 class 则语法错误。

举例:

template <class T>
class MyClass{
    typename T::SubType * ptr;
    ...
};

在这里,typename指出SubType是class T中定义的一个类别,因此ptr是一个指向T::SubType型别的指针。如果没有关键字typename,SubType会被当成一个static成员,于是T::SubType * ptr会被解释为型别T内的数值SubType与ptr的乘积。

SubType成为一个型别的条件是,任何一个用来取代T的型别,其内部必须有一个内部型别(inner type)SubType的定义。

例如,将型别Q当作template的参数。 必要条件是型别Q有如下的内部型别定义:

class Q{
    typedef int SubType;
    ...
};

因此,MyClass的ptr成员应该变成一个指向int型别的指针,子型别SubType也可以成为抽象 数据型别(例如,class):

class Q{
    class SubType;
    ...
};

注意,如果要把一个template中的某个标识符号指定为一种类别,就算是意图显而易见,关键字typename也是不能省略的,因此C++的一般规则是,除了使用typename修饰之外,template内的任何标识符号都被视为一个值而不是一个类别(对象)。

不使用sizeof求一个变量的大小

腾讯/字节跳动笔试的时候,遇到一个问题:使用C实现求一个变量的大小,不使用sizeof。
解法一:

#define size(x) ((char*)(&x+1)-(char*)(&x))

先用&x取得变量的地址,然后用&x+1 跨越该x之后的第一个地址。(指针的加法实际上会移动指向的变量大小的字节)

举个例子:
我们有一个数组:
int a[] = {10,20,30,40,50};
a也是指向数组第一个元素首地址的,也就是指向元素10所在的位置,则(*a)代表着就是值10,那么(a+1)就相当于10+1=11;(a+1)指向的是下一个元素的位置,则((a+1))代表着就是20。
而&a指向的整个数组的首地址,虽然说整个数组的首地址就是第一个元素的首地址,但是整体感觉来说还是不一样的,这样来看,(*a+1)是数组中的第一个元素值加一,而(&a+1)是整个数组加一,下面这个图很好的诠释了这个问题。

在这里插入图片描述
所以说,我们想要求得一个变量的大小的话,只要使(&a+1)减去(&a),然后再转化成字节的形式就可以了。

解法2:
在上面指针的分析当中,(a+1)是指向下一个元素的首地址,那么我们还有一种思路就是这样的,将变量放入到一个数组当中,将(a+1)减去a就得到了变量的大小,再强制转化为字节形式就可以了。

#include <stdio.h>
struct hello{
    char c1;
    int m;
    char c2;
};
int main(void)
{
    //我们需要求结构a的大小
    struct  hello a;
    //则将三个结构a放到数组中
    struct hello b[] = {a,a,a};
    //按照上面的思想进行求取
    int s = (char*)(b+1) - (char*)b;
    //输出结果没有问题
    printf("%d\n",s);

    return 0;
}

public、protected、private 指定继承方式

C++继承的一般语法为:

class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};

[继承方式]默认为 private(成员变量和成员函数默认也是 private)。
public、protected、private 指定继承方式

  1. public继承方式
    • 基类中所有 public 成员在派生类中为 public 属性;
    • 基类中所有 protected 成员在派生类中为 protected 属性;
    • 基类中所有 private 成员在派生类中不能使用。

  2. protected继承方式
    • 基类中的所有 public 成员在派生类中为 protected 属性;
    • 基类中的所有 protected 成员在派生类中为 protected 属性;
    • 基类中的所有 private 成员在派生类中不能使用。

  3. private继承方式
    • 基类中的所有 public 成员在派生类中均为 private 属性;
    • 基类中的所有 protected 成员在派生类中均为 private 属性;
    • 基类中的所有 private 成员在派生类中不能使用。

http://c.biancheng.net/view/2269.html
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值