越深入底层,C/C++的作用就越大,对于底层的了解比别人更具体,工作中由于不是主要语言,用的少,近来有空,重新完整的过了一遍C++的语法和一些内存知识,在这里做一下笔记
基础语法
数据类型
int
/short
/long
, /long long
:默认都为signed
,也可以加unsigned
变为无符号数1
2
3int a = 10;
uint a = 10u;
long a = 10l;float
/double
,long double
:默认都为signed
,也可以加unsigned
变为无符号数bool
:本质是1和0char
(1个字节) /wchar_t
(2个字节)1
2char a = 'a';
wchar_t a = L`a`;
进制表示
- 十进制:
int a = 10;
- 十六进制:
int a = 0xF2;
- 八进制:
int a = 070;
(以0开头)
运算符
- 算数运算符:
+
,-
,*
,/
,%
,++
,--
- 关系运算符:
==
,!=
,>
,<
,>=
,<=
- 逻辑运算符:
&&
,||
,!
- 位运算符:
&
,|
,~
(取反)^
(异或),<<
,>>
- 赋值运算符:
=
,+=
,-=
,*=
,/=
,%=
- 其他运算符:
- 获取变量大小:
sizeof
(不是运算符,为编译器特性) - 三元运算符:
? :
- 取址运算符:
&
- 取值运算符:
*
- 获取变量大小:
运算符重载
1 | class Point { |
+
运算符
- 第一个const:用于限制不能当成左值
(p1 + p2) = Point(1, 2)
- 第二个const:用于让参数接受const和非const变量
- 第三个const:用于声明为const函数,让返回值const支持二次操作
p1 + p2 + p3
+=
运算符
- 返回引用,用于支持
(a += b) = 1
关于位运算
- 左移:移走位补0
- 右移:
逻辑右移
:移走位补0算数右移
:对于有符号数,正数移走位补0,负数移走位补1
具体使用逻辑右移还是算数右移,取决于编译器,所以,
尽量不要使用右移运算符
流程控制
if-else
switch-case
do-while
for
: C++11支持下面集合遍历1
2
3
4int items[] = {1, 2, 3};
for (auto item: items) {
cout << item << endl;
}
头文件重复引用
使用
#define
宏防止重复导入1
2
3
4
5
6
...在文件头使用
#pragma once
也可以防止重复导入(旧的编译器可能不支持),通常放在文件头1
2
3
...
注释
C++的注释与C语言一样
1 | // 单行注释 |
不支持嵌套注释
auto自动类型
在C++11
添加了auto
用于自动推断类型,为编译器特性,免去长长的类型声明
1 | // 编译器会自动推断出i的类型,下面语句等价 |
const
1 | int age = 10; |
const修饰的是右边的内容
const还能用于修饰函数参数,让引用参数接受常量参数,见后面
头文件和实现文件分离
头文件person.h
1 | class Person { |
实现文件person.cpp
1 |
|
命名空间
1 | namespace BM { |
- 命名空间不影响内存布局
- 命名空间可以在代码块使用
- 命名空间可以合并
1 | namespace BM { |
引用(Reference)
1 | // 变量 |
交换两个数
1 | void swap(int &a, int &b) { |
- 引用不可以修改指向,指针可以
本质:引用本质就是指针,只是从编译层面削弱了功能,增强了安全性,下面代码生成最终机器码是一样的
1 | int age = 10; |
数组的引用
1 | int array[] = {1, 2, 3}; |
常引用(const reference)
1 | int a = 10; |
常引用作为函数参数时
1 | // 使用const修饰参数,可以接受常量和变量 |
指针
变量三个重要信息
- 变量的
内容
- 变量存放的
地址
- 变量的
类型
指针变量:专门用来记录变量地址的变量,通过指针变量可以间接访问另一个变量的值
未初始化和非法的指针
1 | // a未初始化 |
NULL指针
:不指向任何东西,表示一种状态,指针变量不用时,或未初始化时,应置位NULL
,在C++11之后,空指针使用nullptr
1 | int *a = nullptr; |
野指针
:指向垃圾(程序逻辑上用不到的指针)内存的指针,通常是被回收资源后未置空的指针,不再使用的指针变量应置为nullptr
值
指针的基本操作:
&
运算符: 取变量地址*
运算符:去指针指向地址的值
指针编译成汇编
1 | int a = 10; |
指针变量原理
1 | Person person; |
可以看出,通过指针针变量访问成员变量生成2条语句,一条取地址的值,第二条才是根据变量的偏移量取变量值,而普通变量访问成员变量会生成1条语句
1 | Person person; |
汇编很多时候(对于栈空间)是通过
偏移量
来操作对象和字段的
内存空间管理
malloc
/free
:C语言的方式new
/delete
:C++的方式,推荐new[]
/delete[]
:数组空间
数组释放
的时候和变量不一样,需要加中括号
1 | // C语言申请空间 |
通常情况下申请的内存空间不会进行初始化(不同平台可能不一样)
1 | int *p1 = (int *)malloc(4); |
如果是对象,new
创建的对象会调用构造函数,而malloc
不会,在C++中,推荐使用new
在堆申请空间
类型转换
C语言的类型转换
1 | int a = 10; |
C++有四中类型转换符
static_cast
: 通常基本数据类型转换,用于非const变量转换成const变量,由于C++有隐式转换,通常不用写1
2
3
4
5
6
7
8
9
10
11
12int a = 10;
// 下面两句等价
double b = static_cast<double>(a);
double b = a;
Person *p1 = new Person();
// 下面两句等价,C++默认会做隐式转换
const Person *p2 = static_cast<const Person *>(p1);
const Person *p2 = p1;dynamic_cast
: 用于多态类型转换,如果不能转换,则返回nullptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Person *p1 = new Person();
Person *p2 = new Student();
// 父类不能赋值给子类,报错
Student *stu1 = p1;
// dynamic_cast会做运行时安全检查,类型不匹配时,会返回NULL
Student *stu2 = dynamic_cast<Student *>(p1);
Student *stu3 = dynamic_cast<Student *>(p2);
// 汇编:会调用一个函数进行
// call __RTDynamicCast(0C14ABh) ; 调用方法判断类型,返回值放到eax,可能为NULl,可能为对象的值
// add esp, 14h
// mov dword ptr [stu1], eax
// stu2 = NULLL
// stu3 = p2const_cast
: 将const常量转换成非常量,有安全风险1
2
3
4
5
6
7const Person *p1 = new Person();
// 无法直接转换,报错
Person *p2 = p1;
// 下面两种转换等价
Person *p2 = const_cast<Person *>p1;
Person *p3 = (Person *)p1;reinterpret_cast
: 纯二进制拷贝,没有类型检查1
2
3
4
5
6
7
8
9
10int a = 10;
// 0A 00 00 00
double b = a;
// 00 00 00 00 00 00 24 40
// 由于double类型和int的表示10不一样,所以下面b != 10;
double c = reinterpret_cast<double&>(a);
// 0A 00 00 00 CC CC CC CC
// -9.25596e+61
多余对象拷贝
1 | Point func() { |
上面代码如果编译器不做优化的话,会调用3
次Point的构造函数
- 在func函数栈构造Point
- 从func函数栈返回到main函数栈,会把返回值,通过拷贝构造函数拷贝到main函数栈
- main函数中,返回值赋值给p,会调用Point的拷贝构造函数
编译器在编译的时候会做返回值优化(RVO),不会造成多次拷贝,可以通过
-fno-elide-constructors
关闭该优化
C++程序内存分布
- 栈
- 堆
- 全局区/静态区: 可读写
- 常量区: 只读
- 代码段: 只读
函数
函数重载
指函数名相同的函数,函数参数类型不同
或函数参数顺序不同
或函数参数个数不同
,构成函数重载,函数重载与返回值类型无关(C语言不支持函数重载)
1 | // 下面函数都构成重载 |
本质:C++使用了
name mangling
或name decoration
的技术,C++编译器在编译的时候会对函数名进行改编,修饰,不同的编译器修饰的规则可能不同,例如上面sum函数在VC++
会编译下面方法名
1 | sum0, sum1, sum2, sum3, sum4, sum5, sum6 |
默认参数
C++支持默认参数,如果有声明和实现,默认参数必须放在声明上
1 | int sum(int a = 1, int b = 2); |
默认参数可以是常亮,全局符号
本质:编译器在编译阶段根据默认参数补完传参,也就是
sum(1)
和sum(1, 2)
编译后的汇编代码是一样的
extern “C”
使用extern "C"
修饰的代码会按照C语言的方式编译
1 | // func会被编译为C语言的方法 |
如果函数声明和实现分开,声明需要加
extern "C"
,实现不加
由于C++的函数有name mangling
,extern "C"
通常在C语言和C++混编的时候用到
C语言不支持extern "C"
,可以使用__cplusplus
加判断
1 | // C语言不支持extern符号 |
内联函数(inline)
1 | inline void add(int a, int b) { return a + b; } |
函数展开,类似于define的效果,省去函数调用开辟栈空间的操作
- 函数代码体积小(小于10行)
- 函数频繁使用
递归函数不会被编译为内联函数,即使声明了
其他知识点
关于补码
对于有符号数的正负3
十进制+3
: 00000000 00000000 00000000 00000011
十进制-3
: 10000000 00000000 00000000 00000011
上面的表示并不方便计算(两个数想加),实际上,计算机使用补码
的方式表示有符号数
,在计算上有很大的优势
十进制+3
: 00000000 00000000 00000000 00000011
十进制-3
: 11111111 11111111 11111111 11111101
使用补码是为了:用加法计算减法,性能更优,CPU不用单独再实现一个减法运算器
字节序(Byte Ordering)
- 大端法(Big Endian): 高位字节在前,低位字节在后,这是人类读写数值的方法
- 小端法(Little Endian): 低位字节在前,高位字节在后
为什么会有小端字节序?
答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序
为什么会有大端字节序?
人类习惯读写是大端字节序(从左到右)。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
内存对齐
1 | typedef struct { |
- C++的结构体是按变量的定义顺序进行存储的,也就是
a -> b -> c
- CPU从内存中读取数据时,有一个最小读取单位,例如64位的CPU,从内存的0地址开始,0-63bit的数据可以一次IO读取出来,64-127bit的数据也可以一次读取出来,每个次读取的单位是64,CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO,为了提高CPU读取效率,减少IO次数,编译器在编译代码的时候,会考虑到内存对齐的情况,例如上面结构体A会被编译为24个字节
如果结构体A不进行内存对齐的话,变量b与变量a靠在一起,就会导致,读取在变量b的时候,需要两次IO
命名法
- 匈牙利命名法:
int iMyValue
,第一个小写字母表示类型 - Cammel命名法:
int myAge
- Pascal命名法:
int MyAge
编码
Unicode: 表示所有语言
- ASCII: 1byte表示一个字符,存储效率高,存储的字符有限
- UTF-8:1byte表示一个字符,可以兼容ASCII码,存储效率高,可变长(随机访问效率低),无字节序的问题(可作为外部编码),如网络传输普遍使用
- UTF-16:2byte表示一个字符,定长(随机访问效率高),有字节序的问题(不可作为外部编码),但不能表达所有的字符
- UTF-32:4byte表示一个字符,定长(随机访问效率高),有字节序的问题(不可作为外部编码),可以表达目前已存在的所有的字符
下一篇主要是面向对象相关的知识点