0%

关于c++基础的一些注意事项

C++ 编译链接过程

1、预处理器:将.c 文件转化成 .i文件,使用的gcc命令是:gcc –E,对应于预处理命令cpp;

2、编译器:将.c/.h文件转换成.s文件,使用的gcc命令是:gcc –S,对应于编译命令 cc –S;

3、汇编器:将.s 文件转化成 .o文件,使用的gcc 命令是:gcc –c,对应于汇编命令是 as;

4、链接器:将.o文件转化成可执行程序,使用的gcc 命令是: gcc,对应于链接命令是 ld;

5、加载器:将可执行程序加载到内存并进行执行,loader和ld-linux.so。

由不同编译器创建的二进制模块很可能无法正确链接,在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的(或同一标准的编译器)

编译器前端和后端

编译器粗略分为词法分析语法分析类型检查中间代码生成代码优化目标代码生成目标代码优化

把中间代码生成及之前阶段划分问编译器的前端,那么后端与前端是独立的。后端只需要一种中间代码表示,可以是三地址代码或四元式等,而这些都与前端生成的方式无关。

编译器的前端将不同的高级编程语言经过词法分析、语法分析转化为与前端语言无关的中间表示。有了与前端语言无关的中间表示,一个编译器就可以支持编译多种高级语言。

按照这个分类,自己动手编写编译器,可以不必从头开始了。使用LLVM,我们可以做一个前端,然后和LLVM后端对接。

大端(Big Endian/Big Endiayin)

数据的高字节部分放在内存中的低字节地址,数据的低字节部分放在内存中的高字节地址

例如:大端.png

小端(Little Endian)

数据的高字节部分放在内存中的高字节地址,数据的低字节部分放在内存中的低字节地址

例如:小端.png

类型转换

不同基本操作数进行运算会隐式的将内存占用较少的类型转换成占用较多的操作数类型

尽量不要使用隐式类型转换,即使是隐式的数据类型转换是安全的,因为隐式类型数据转换降低了程序的可读性。尽量使用强制(显示)类型转换

占用空间大的数据的指针强制转换成占用空间小的数据的指针,会发生内存截断,将占用空间小的数据的指针强制转换成占用空间大的数据的指针,会发生内存扩张。例如

1
2
3
4
5
6
7
doule d5 = 100.0;

int *pInt = (int*)&d5;

int i4 = 100;

double pDbl = (double*)&i4;

内存截断:内存截断.png

内存扩张:内存扩张.png

发生内存扩张时,往扩张的内存写数据会发生运行时错误

标识符

标准C语言规定,编译器只取前31个字符作为有效的标示符,而标准C++取前255个字符作为有效的标识符

引用类型

引用变量在声明时必须被赋值,引用变量和被引用的变量的地址是相同的

1
2
3
4
int a = 1;
int &b = a;
printf("the address of a is %p and the address of b is %p", &a, &b);
//打印的结果地址相同

常量

对于指针类型和引用类型,将非const值赋值给const变量是合法的,但是反之则是非法的

1
2
3
4
5
6
7
int a = 10;

const int &b = a; //ok

int &c = b;//错误,const int &类型,不能直接赋给int &类型

int *d = &b;//错误,const int *类型,不能直接赋给int *类型

字面常量

字面常量:直接出现的数字、字符、字符串等,只存在基本数据类型的字面常量,字面常量只能引用,不能修改。除字符串外,你无法取一个字面常量的地址,例如:

1
int *p = &5;//语句错误

并且当你试图通过常量字符串的地址修改其中的字符时就会报告“只读错误”(此错误为运行时错误)例如:

1
2
3
char *pChar = "abcdef";

*(pChar + 2) = 'k';//错误,不能修改字面常量的内存单元

符号常量

符号常量分为用#define定义的宏常量和用const定义的常量。

使用#define宏定义的符号常量在进入编译阶段前就已经被替代为所代表的字面常量了,因此宏常量本质上是字面常量。

取const符号常量的地址或引用时,对于基本数据类型的const常量,编译器会重新在内存中创建一个拷贝,你通过其地址访问到的时这个拷贝而非原始的符号常量,例如:
1
2
3
4
5
6
7
const long lng = 10;

long *pl = (long *)&lng;

*pl = 11;

std::cout<<lng<<"-"<<*pl;//输出为10-11
对于构造类型的const常量,实际上是编译时不允许修改的变量,因此可以绕过编译器的静态类型安全检查机制,就可以在运行时修改其内存单元,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Integer {
int m_lng
}


const Integer int_1;

//int_1.m_lng = 1000;//编译错误

Integer *pInt = (Integer *)&int_1;

pInt->m_lng = 1000;

std::cout << int_1.m_lng << "-" << pInt->m_lng;//输出为1000-1000
在C++程序中要尽量使用const来定义符号常量,包括字符串常量。 ### const char* p 和 char* const p, const char* const p 这三个的记忆方法为从右到左读: 1. const char* p读作:p is a pointer to const char(`const char* p等同于char const* p`)。对于const char* p,不能使用指针改变指针指向的内容,但是可以改变指针自身的值。 2. char* const p读作:p is a const pointer to char。对于char* const p,可以使用指针改变指针指向的内容,但是不能改变指针自身的值。 3. const char* const p 读作:p is a const pointer to const char。对于const char* const p,既不能使用指针改变指针指向的 # 函数 ## 函数参数传递规则 * 一般的,输出参数放在参数列表的前面,输入参数放在后面,并且不要交叉出现输入输出参数。 * 如果参数是指针,且仅做输入用,则应在类型前加const,以防止该指针指向的内存单元在函数体内无意中被修改。如果输入参数以值传递的方式传递对象,则宜改用"const &"方式来传递,因为引用的创建和销毁不会调用对象的构造和析构函数,从而可提高效率。 ## 返回值的规则 * 不要将正常值和错误标志混在一起返回。建议正常值用输出参数获得,而错误标志用return语句返回。 ## 函数内部实现的规则 * return语句不可返回指向“栈内存”的指针或者引用,因为该内存单元在函数体结束时会被自动释放。 ## 使用const提高函数的健壮性 如果参数用于输出,不论参数是什么数据类型,也不论是采用“指针传递”还是“引用传递”,都不能加const参数,否则该参数将失去输出功能。const只能修饰输入参数。 我个人认为不要将函数的返回值用const修饰,因为函数的作用应该和其他代码是弱耦合的,而且函数返回值应该怎么使用应该由函数外部的代码来决定。 `函数需要返回一个数组时,最好不要返回指向数组的指针,因为如果这么做的话,这个数组要么必须是new出来的,要么必须是static 类型的。最好将需要返回的数组作为函数参数传入。` 不管指针变量是全局的还是局部的、静态的还是非静态的,应当在声明它的同时初始化它,要么赋予它一个有效的地址,要么赋予它NULL。 # assert assert的宏体全部被条件编译命令\#ifdef _DEBUG和 \#endif所包含,因此assert只有在Debug版本才有效

指针运算

  • 指针自增(++),表示它指向了序列中的后一个元素

  • 指针自减(—),表示它指向了序列中的前一个元素

  • 指针加一个正整数i,表示它向后递进i个元素

  • 指针减一个正整数i,表示它向前递进i个元素

  • 两个同类型指针相减,表示计算它们之间的元素个数

指针加/减一个正整数i,其含义并不是在其值上直接加/减i,还要包含所指对象的字节数信息。

不能对void*类型指针使用“*”来取所指的变量

数组

任何数组,不论是静态声明的还是动态创建的,其所有元素在内存中都是连续字节存放的,也就是说保存在一大块连续的内存区中,std::vector的所有元素对象在内存中也是连续存放的。

创建动态二维数组的方法

1
2
3
4
5
6
7
int **p = new int*[n];

for(int i = 0; i < n; ++i) {

p[i] = new int[m];

}

一个很不符合逻辑的表达式

1
2
3
4
5
6
7
int a = 0;

int b = 1;

int c = 2;

a + b = c;//这句话会先执行a+b,将a+b的值放在一个临时对象中,然后将c的值赋予该临时对象。

最后一个表达式没有任何实际意义,它一般是错误将判断相等表达式“==”错误写成“=”的结果

endl

endl是一个函数模板,它实例化之后变成一个模板函数,其作用是插入换行符并刷新输出流。其中刷新输出流指的是将缓冲区的数据全部传递到输出设备并将输出缓冲区清空。

避免头文件循环引用

1
2
3
4
5
6
7
#ifndef HEADER_H

#define HEADER_H

//place include file content and your code here

#endif

  • #ifdef 只判断是否定义了某个宏

  • #if 不仅判断是否定义了宏,而且还判断宏是否为真

  • #undef 用于取消定义的宏

利用宏转字符串

1
#define TO_STR(a) #a

在宏定义汇总使用#表示将符号转换为对应的字符串,例如使用上面的宏TO_STR(test)的结果就是"test"

利用宏链接符号

1
#define Contact(a) a##_1

在宏定义中使用##表示连接,例如使用上面的宏Contact(test)的结果就是test_1