0%

重学C++学习笔记(一)

越深入底层,C/C++的作用就越大,对于底层的了解比别人更具体,工作中由于不是主要语言,用的少,近来有空,重新完整的过了一遍C++的语法和一些内存知识,在这里做一下笔记

基础语法

数据类型

  • int / short / long, / long long:默认都为signed,也可以加unsigned变为无符号数

    1
    2
    3
    int a = 10;
    uint a = 10u;
    long a = 10l;
  • float / double, long double:默认都为signed,也可以加unsigned变为无符号数

  • bool:本质是1和0

  • char(1个字节) / wchar_t(2个字节)

    1
    2
    char a = 'a';
    wchar_t a = L`a`;

进制表示

  • 十进制:int a = 10;
  • 十六进制:int a = 0xF2;
  • 八进制: int a = 070;(以0开头)

运算符

  • 算数运算符: +,-,*,/,%,++,--
  • 关系运算符: ==, !=, >, <, >=, <=
  • 逻辑运算符: &&, ||, !
  • 位运算符: &, |, ~(取反) ^(异或), <<, >>
  • 赋值运算符: =, +=, -=, *=, /=, %=
  • 其他运算符:
    • 获取变量大小:sizeof(不是运算符,为编译器特性)
    • 三元运算符:? :
    • 取址运算符: &
    • 取值运算符: *

运算符重载

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Point {
// 友元函数,为了访问私有变量
const Point operator-(const Point a, const Point b);
private:
int m_x;
int m_y;
public:
Point(int x, int y): m_x(x), m_y(y) { }

// 加号运算符
cosnt Point operator+(const Point &point) const {
return Point(m_x + point.m_x, m_y + point.m_y);
}

// 返回引用,用于支持(a += b) = 1
Point &operator+=(const Point &point) {
m_x += point.m_x;
m_y += point.m_y;
return *this;
}

// 单目运算: Point p2 = -p1;
const Point operator-() const {
return Point(-m_x, -m_y);
}

// 前置运算符: Point p2 = ++p1;
Point &operator++() {
m_x += 1;
m_y += 1;
return *this;
}

// 后置运算符: Point p2 = ++p1;
const Point operator++(int) {
Point temp(m_x, m_y);
m_x += 1;
m_y += 1;
return temp;
}
};

// 定义在外面
const Point operator-(const Point a, const Point b) {
return Point(a.m_x - b.m_x, a.m_y + b.m_y);
}

int main() {
auto p1 = Point(1, 2);
auto p2 = Point(3, 4);

// p3 = Point(4, 6);
auto p3 = p1 + p2;

// p4 = Point(2, 2);
auto p4 = p2 - p1;
return 0;
}

+运算符

  • 第一个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
    4
    int items[] = {1, 2, 3};
    for (auto item: items) {
    cout << item << endl;
    }

头文件重复引用

  1. 使用#define宏防止重复导入

    1
    2
    3
    4
    5
    6
    #ifndef __HEADER_H
    #define __HEADER_H

    ...

    #endif
  2. 在文件头使用#pragma once也可以防止重复导入(旧的编译器可能不支持),通常放在文件头

    1
    2
    3
    #pragma once

    ...

注释

C++的注释与C语言一样

1
2
3
4
5
6
// 单行注释

/*
多行注释
多行注释
*/

不支持嵌套注释

auto自动类型

C++11添加了auto用于自动推断类型,为编译器特性,免去长长的类型声明

1
2
3
4
// 编译器会自动推断出i的类型,下面语句等价
auto i = unique_ptr<int>(new int(10));

unique_ptr<int> i = unique_ptr<int>(new int(10));

const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int age = 10;

// p1不是常量,*p1是常量
const int *p1 = &age;
// 与p1一样
int const *p2 = &age;

// p3是常量,*p3不是常量
int * const p3 = &age;

// p4是常量,*p4是常量
const int * const p4 = &age;
// 与p4一样
int const * const p5 = &age;

const修饰的是右边的内容
const还能用于修饰函数参数,让引用参数接受常量参数,见后面

头文件和实现文件分离

头文件person.h

1
2
3
4
5
6
7
8
9
class Person {
private:
int m_age;
public:
void setAge(int age);
int getAge();
Person();
~Person();
}

实现文件person.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Person.h"

void Person::setAge(int age) {
m_age = age;
}
int Person::getAge() {
return m_age;
}
Person::Person() {
m_age = 0;
}
Person::~Person() {
// ...
}

命名空间

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
namespace BM {
class Person {
int m_age;
int m_height;
}
void func() {

}

// 命名空间支持嵌套
namespace SS {
// ...
}
}

// 全局命名空间
// 可以通过`::func`访问
// 默认情况下不用加`::`
void func() {

}

BM::Person person = new BM::Person();

// 只用命名空间的部分成员
using BM::Person;

// 使用using之后不用加前缀
using namespace BM;

// 访问命名空间里面的方法
BM::func()
// 访问全局命名空间
::func()
  • 命名空间不影响内存布局
  • 命名空间可以在代码块使用
  • 命名空间可以合并
1
2
3
4
5
6
7
8
9
10
11
namespace BM {
int g_age;
}
namespace BM {
int g_height;
}
// 上面与下面等价
namespace BM {
int g_age;
int g_height;
}

引用(Reference)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 变量
int age = 10;

// 引用变量,相当于age的别名,定义的时候就要赋值
int &rage = age;

// 相当于:age = 20
rage = 20;

int height = 20;

// rage赋值后不能修改,下面相当于rage = 20,rage还是引用age
rage = height

交换两个数

1
2
3
4
5
6
7
8
9
10
11
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}

int a = 10;
int b = 20;
swap(a, b);

// a为20,b为10
  • 引用不可以修改指向,指针可以

本质:引用本质就是指针,只是从编译层面削弱了功能,增强了安全性,下面代码生成最终机器码是一样的

1
2
3
4
5
6
7
8
9
int age = 10;

// 指针
int *p = &age;
*p = 30

// 引用
int &ref = age;
ref = 30;

数组的引用

1
2
3
4
5
6
7
8
9
10
int array[] = {123};

// 需要指定数组大小
int (&ref)[3] = array;

// 使用指针接受引用
int * const &ref = array;

// 使用auto
auto &ref = array;

常引用(const reference)

1
2
3
4
5
int a = 10;
const int &b = a;

// 无法修改引用的值,报错
b = 20;

常引用作为函数参数时

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用const修饰参数,可以接受常量和变量
int sum(const int &a, const int &b) {
return a + b
}

int p = 10;
int q = 20;

// 可以接受变量
sum(p, q);

// 可以接受常量
sum(p, 10);

指针

变量三个重要信息

  • 变量的内容
  • 变量存放的地址
  • 变量的类型

指针变量:专门用来记录变量地址的变量,通过指针变量可以间接访问另一个变量的值

未初始化和非法的指针

1
2
3
4
// a未初始化
int *a;
// 直接修改a指向的值,会出现不可预测的问题
*a = 12;

NULL指针:不指向任何东西,表示一种状态,指针变量不用时,或未初始化时,应置位NULL,在C++11之后,空指针使用nullptr

1
2
3
4
5
int *a = nullptr;

if (a != nullptr) {
*a = 12;
}

野指针:指向垃圾(程序逻辑上用不到的指针)内存的指针,通常是被回收资源后未置空的指针,不再使用的指针变量应置为nullptr

指针的基本操作:

  • &运算符: 取变量地址
  • *运算符:去指针指向地址的值

指针编译成汇编

1
2
3
4
5
6
7
8
9
10
int a = 10;
// mov dword ptr [ebp-0Ch], 10 ; a的地址为[ebp-0Ch], a = 10

int *p = &a;
// lea eax, [epb-0Ch] ; 取a的地址赋值到eax
// mov dword ptr [ebp-18h], eax ; p的地址为[epb-18h],把a的地址赋值给指针p,int *p = &a;

*p = 20;
// mov eax, dword ptr [ebp-18h] ; eax = &a;
// mov dword ptr [eax], 20 ; 取出eax指向的地址,复制20,相当于 age = 20;

指针变量原理

1
2
3
4
5
6
7
8
9
10
11
12
13
Person person;
person.age = 10;
// mov dword ptr [ebp-14h], 0Ah
person.height = 20;
// mov dword ptr [ebp-10h], 14h

Person *p = &person;
// lea eax, [ebp-14h] ; 将person的地址赋值给寄存器eax
// mov dword ptr [epb-20h], eax ; 将寄存器eax的地址赋值给变量p(epb-20h)

p->age = 10;
// mov eax, dword ptr [epb-20h] ; 将变量p存放的地址赋值给eax,eax = &person
// mov dword ptr [eax], 0Ah ; 将eax指向对象赋值10

可以看出,通过指针针变量访问成员变量生成2条语句,一条取地址的值,第二条才是根据变量的偏移量取变量值,而普通变量访问成员变量会生成1条语句

1
2
3
4
5
6
7
8
9
10
11
Person person;
person.m_id = 10;
person.m_age = 20;

// 这里取的是 m_age
Person *p = (Person *)&person.m_age;
p->m_id = 30;

cout << person.m_id << endl;
cout << person.m_age << endl;
// 输出: 10, 30

汇编很多时候(对于栈空间)是通过偏移量来操作对象和字段的

内存空间管理

  • malloc / free:C语言的方式
  • new / delete:C++的方式,推荐
  • new[] / delete[]:数组空间

数组释放的时候和变量不一样,需要加中括号

1
2
3
4
5
6
7
8
9
10
11
12
13
// C语言申请空间
int *p1 = (int *)malloc(4);
*p1 = 20;
free(p1);

// C++申请空间
int *p2 = new int;
*p2 = 10;
delete p2;

// 如果是数组,delete也要加中括号
char *p3 = new char[4];
delete [] p3;

通常情况下申请的内存空间不会进行初始化(不同平台可能不一样)

1
2
3
4
5
6
7
8
int *p1 = (int *)malloc(4);
// 初始化
p1 = 0;

int size = sizeof(int) * 10;
int *p2 = (int *)malloc(size);
// 初始化,把所有空间清零
memset(p2, 0, size);

如果是对象,new创建的对象会调用构造函数,而malloc不会,在C++中,推荐使用new在堆申请空间

类型转换

C语言的类型转换

1
2
3
int a = 10;
long b = (int)a;
double c = double(a)

C++有四中类型转换符

  • static_cast: 通常基本数据类型转换,用于非const变量转换成const变量,由于C++有隐式转换,通常不用写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int 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
    17
    Person *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 = p2
  • const_cast: 将const常量转换成非常量,有安全风险

    1
    2
    3
    4
    5
    6
    7
    const 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
    10
    int 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
2
3
4
5
6
7
8
9
10
11
Point func() {
// 1. 调用Point默认构造函数
Point p;
return p;
}

void main() {
// 2. 从func函数栈拷贝Point到main函数栈
// 3. 调用Point拷贝构造函数
Point p = func();
}

上面代码如果编译器不做优化的话,会调用3次Point的构造函数

  1. 在func函数栈构造Point
  2. 从func函数栈返回到main函数栈,会把返回值,通过拷贝构造函数拷贝到main函数栈
  3. main函数中,返回值赋值给p,会调用Point的拷贝构造函数

编译器在编译的时候会做返回值优化(RVO),不会造成多次拷贝,可以通过-fno-elide-constructors关闭该优化

C++程序内存分布

  • 全局区/静态区: 可读写
  • 常量区: 只读
  • 代码段: 只读

函数

函数重载

指函数名相同的函数,函数参数类型不同函数参数顺序不同函数参数个数不同,构成函数重载,函数重载与返回值类型无关(C语言不支持函数重载)

1
2
3
4
5
6
7
8
9
10
11
// 下面函数都构成重载
int sum(int a, int b);
int sum(long a, long b);
int sum(int a, int b, int c);

int sum(int a, long b);
int sum(long a, int b);

// const引用(指针)与非const引用(指针)构成重载,下面两个函数是不同函数
int sum(int &a, int &b);
int sum(const int &a, const int &b);

本质:C++使用了name manglingname decoration的技术,C++编译器在编译的时候会对函数名进行改编,修饰,不同的编译器修饰的规则可能不同,例如上面sum函数在VC++会编译下面方法名

1
sum0, sum1, sum2, sum3, sum4, sum5, sum6

默认参数

C++支持默认参数,如果有声明和实现,默认参数必须放在声明上

1
2
3
4
5
int sum(int a = 1, int b = 2);

int sum(int a, int b) {
return a + b;
}

默认参数可以是常亮,全局符号

本质:编译器在编译阶段根据默认参数补完传参,也就是sum(1)sum(1, 2)编译后的汇编代码是一样的

extern “C”

使用extern "C"修饰的代码会按照C语言的方式编译

1
2
3
4
5
6
7
8
9
10
// func会被编译为C语言的方法
extern "C" void func() {
// ...
}
extern "C" {
// C语言代码
void func() {
// ...
}
}

如果函数声明和实现分开,声明需要加extern "C",实现不加
由于C++的函数有name manglingextern "C"通常在C语言和C++混编的时候用到
C语言不支持extern "C",可以使用__cplusplus加判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// C语言不支持extern符号
#ifdef __cplusplus
extern "C" {
#endif
// 引用C头文件
#include "header.h"
#ifdef __cplusplus
}
#endif

#ifdef __cplusplus
extern "C" {
#endif
int sum(int a, int b);
int delta(int a, int b);
int divide(int a, int b);
#ifdef __cplusplus
}
#endif

内联函数(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): 低位字节在前,高位字节在后

为什么会有小端字节序?

答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序

为什么会有大端字节序?

人类习惯读写是大端字节序(从左到右)。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

引用:https://www.cnblogs.com/gremount/p/8830707.html

内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
int a;
double b;
short c;
} A;
typedef struct {
int a;
short b;
double c;
} B;

sizeof(A); // 24
sizeof(B); // 26
  • 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表示一个字符,定长(随机访问效率高),有字节序的问题(不可作为外部编码),可以表达目前已存在的所有的字符

下一篇主要是面向对象相关的知识点