C/C++系列记录(持续更新)
文章目录
Linux C编程一站式学习
运算符
- ++i这个表达式相当于i = i + 1,i加1了,返回值也是加1了
i++,i加1了,但是返回值并没有加1
a+++++b这个表达式如何理解?应该理解成a++ ++ +b还是a++ + ++b,还是a + ++ ++b呢?应该按第一种方式理解。参考编译词法解析和语法解析过程1
- 移位运算符(Bitwise Shift)包括左移<<和右移>>。在一定的取值范围内,将一个整数左移1位相当于乘以2。右移1位相当于除以2。
switch语句
等价于if/else,但会有更优化
case只能比较常量
switch语句可以产生具有多个分支的控制流程。它的格式是:
switch (控制表达式) {
case 常量表达式: 语句列表
case 常量表达式: 语句列表
...
default: 语句列表
}
-
case后面跟表达式的必须是常量表达式,这个值和全局变量的初始值一样必须在编译时计算出来。
-
第 2 节 “if/else语句”讲过浮点型不适合做精确比较,所以C语言规定case后面跟的必须是整型常量表达式。
-
进入case后如果没有遇到break语句就会一直往下执行,后面其它case或default分支的语句也会被执行到,直到遇到break,或者执行到整个switch语句块的末尾。通常每个case后面都要加上break语句
理解return
在有返回值的函数中,return语句的作用是提供整个函数的返回值,并结束当前函数返回到调用它的地方。
在没有返回值的函数中也可以使用return语句,例如当检查到一个错误时提前结束当前函数的执行并返回。
结构体
编程语言中,最基本的、不可再分的数据类型称为基本类型(Primitive Type),例如整型、浮点型;
根据语法规则由基本类型组合而成的类型称为复合类型(Compound Type),例如字符串是由很多字符组成的。
有些场合下要把复合类型当作一个整体来用,而另外一些场合下需要分解组成这个复合类型的各种基本类型,复合类型的这种两面性为数据抽象(Data Abstraction)奠定了基础
在学习一门编程语言时要特别注意以下三个方面:
- 这门语言提供了哪些Primitive,比如基本类型,比如基本运算符、表达式和语句。
- 这门语言提供了哪些组合规则,比如基本类型如何组成复合类型,比如简单的表达式和语句如何组成复杂的表达式和语句。
- 这门语言提供了哪些抽象机制,包括数据抽象和过程抽象(Procedure Abstraction)
struct complex_struct {
double x, y;
};
struct complex_struct {
double x, y;
} z1, z2;
struct complex_struct z3, z4;
struct complex_struct z = { 3.0, 4.0 };
- 抽象
组合使得系统可以任意复杂,而抽象使得系统的复杂性是可以控制的,任何改动都只局限在某一层,而不会波及整个系统。著名的计算机科学家Butler Lampson说过:“All problems in computer science can be solved by another level of indirection.”这里的indirection其实就是abstraction的意思。
编码风格
命名风格
标识符命名应遵循以下原则:
- 标识符命名要清晰明了,可以使用完整的单词和易于理解的缩写。短的单词可以通过去元音形成缩写,较长的单词可以取单词的头几个字母形成缩写。总结出一些缩写惯例,例如:
org | 缩写 |
---|---|
count | cnt |
block | blk |
length | len |
message | msg |
trans… | x… |
-
内核编码风格规定变量、函数和类型采用全小写加下划线的方式命名,常量(比如宏定义和枚举常量)采用全大写加下划线的方式命名,比如上一节举例的函数名radix_tree_insert、类型名struct radix_tree_root、常量名RADIX_TREE_MAP_SHIFT等。
-
全局变量和全局函数的命名一定要详细,不惜多用几个单词多写几个下划线,例如函数名radix_tree_insert,因为它们在整个项目的许多源文件中都会用到,必须让使用者明确这个变量或函数是干什么用的。局部变量和只在一个源文件中调用的内部函数的命名可以简略一些,但不能太短。尽量不要使用单个字母做变量名,只有一个例外:用i、j、k做循环变量是可以的。
-
禁止用汉语拼音做标识符,可读性极差。
函数
每个函数都应该设计得尽可能简单,简单的函数才容易维护。应遵循以下原则:
- 一个函数只是为了做好一件事情,不要把函数设计成用途广泛、面面俱到的,这样的函数肯定会超长,而且往往不可重用,维护困难。
- 函数内部的缩进层次不宜过多,一般以少于4层为宜。如果缩进层次太多就说明设计得太复杂了,应考虑分割成更小的函数(Helper Function)来调用。
- 函数不要写得太长,建议在24行的标准终端上不超过两屏,太长会造成阅读困难,如果一个函数超过两屏就应该考虑分割函数了。[CodingStyle]中特别说明,如果一个函数在概念上是简单的,只是长度很长,这倒没关系。。
- 执行函数就是执行一个动作,函数名通常应包含动词,例如get_current、radix_tree_insert。
- 比较重要的函数定义上侧必须加注释,说明此函数的功能、参数、返回值、错误码等。
- 另一种度量函数复杂度的办法是看有多少个局部变量,5到10个局部变量已经很多了。
indent工具可以把代码格式化成某种风格
indent -kr -i8 main.c
调试工具gdb
调试的基本思想仍然是“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环,根据现象如何假设错误原因,以及如何设计新的现象去验证假设
- 单步执行和跟踪函数调用
- 断点
- 观察点
- 段错误
完整编译C过程
- 汇编与C
gcc main.c -g # -g
objdump -dS a.out #objdump反汇编时可以把C代码和汇编代码穿插起来显示
要查看编译后的汇编代码,其实还有一种办法是gcc -S main.c,这样只生成汇编代码main.s,而不生成二进制的目标文件
gcc -S main.c
cat main.s
汇编程序的入口是_start,而C程序的入口是main函数
以前我们说main函数是程序的入口点其实不准确,_start才是真正的入口点,而main函数是被_start调用的
- 编译连接详细过程,参考这里
#1. 预处理preprocess (# 加头文件,替换宏等)
gcc -E test.c -o test.i
#2. 编译compiling,生成汇编代码(仍然是文本)
gcc -S test.i -o test.s
#3. 汇编Assembling, 生成目标文件(二进制)
gcc -c test.s -o test.o
#4. 链接Linking,生成可执行文件(二进制)
gcc test.o -o test
这些选项都可以和-o搭配使用,给输出的文件重新命名而不使用gcc默认的文件名(xxx.c、xxx.s、xxx.o和a.out)
静态库和动态库
参考这里
一种常以.a(archived)为后缀,为静态库;另一种以.so(shared object)为后缀,为动态库
目标文件常常按照特定格式来组织,在linux下,它是ELF格式(Executable Linkable Format,可执行可链接格式),而在windows下是PE(Portable Executable,可移植可执行)。
而通常目标文件有三种形式:
- 可执行目标文件。即我们通常所认识的,可直接运行的二进制文件。
- 可重定位目标文件。包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件。
- 共享目标文件。它是一种在加载或者运行时进行链接的特殊可重定位目标文件
readelf -h / lib/x86_64-linux-gnu/libm.so .6
readelf -h main #查看ELF文件头
- 使用静态库编译
gcc - static-o main main.o -lm // -Im 一定要放在最后,编译器从左到右依次找哪些没有被定义并在后面找到链接
由于最终生成的可执行文件中已经包含了exp相关的二进制代码,因此这个可执行文件在一个没有libm.a的linux系统中也能正常运行。但是文件大小更大些,运行速度当然要快些
- 默认就是使用动态库编译
gcc -o main main.o -lm
它并不在链接时将需要的二进制代码都“拷贝”到可执行文件中,而是仅仅“拷贝”一些重定位和符号表信息,这些信息可以在程序运行时完成真正的链接过程
ldd main #可以查看链接了哪些动态库
动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活
变量的存储和生命周期
#include <stdio.h>
const int A = 10;
int a = 20;
static int b = 30;
int c;
int main(void)
{
static int a = 40;
char b[] = "Hello world";
register int c = 50;
printf("Hello world %d\n", c);
return 0;
}
变量的生存期(Storage Duration,或者Lifetime)分为以下几类:
- 静态生存期(Static Storage Duration),具有外部或内部链接属性,或者被static修饰的变量,在程序开始执行时分配和初始化一次,此后便一直存在直到程序结束。这种变量通常位于.rodata,.data或.bss段,例如上例中main函数外的A,a,b,c,以及main函数里的a。
- 自动生存期(Automatic Storage Duration),链接属性为无链接并且没有被static修饰的变量,这种变量在进入块作用域时在栈上或寄存器中分配,在退出块作用域时释放。例如上例中main函数里的b和c。
- 动态分配生存期(Allocated Storage Duration),以后会讲到调用malloc函数在进程的堆空间中分配内存,调用free函数可以释放这种存储空间。
定义和声明
- 用static关键字声明具有Internal Linkage。有些函数只在模块内部使用而不希望被外界访问到,则声明为Internal Linkage的。这会保护模块内部的变量/函数
- 用external关键字声明具有External Linkage。有些函数是提供给外界使用的,也称为导出(Export)给外界使用,这些函数声明为External Linkage的。
/* stack.c */
static char stack[512];
static int top = -1;
void push(char c)
{
stack[++top] = c;
}
char pop(void)
{
return stack[top--];
}
int is_empty(void)
{
return top == -1;
}
/* main.c */
#include <stdio.h>
//对于函数的 extern声明可写可不写,会自动外部链接stack.c中的定义中。但是变量如果有extern声明,必须写
extern void push(char);
extern char pop(void);
extern int is_empty(void);
/*需要注意的是,如果单独编译main.c,没有上面的函数声明,那么编译器会warning: implicit declaration of function ‘push’。
编译器只能通过隐式声明来猜测函数原型,隐式声明只能规定返回值都是int型的,但这种猜测往往会出错。*/
int main(void)
{
push('a');
push('b');
push('c');
while(!is_empty())
putchar(pop());
putchar('\n');
return 0;
}
头文件(值得看看)
为了简化main.c里函数的声明,使用头文件,然后#include进来。
还有个作用是,函数定义的算法一般被编译成二进制发布出来(不公开源码的),而函数声明放在头文件.h里声明并公开,能够被调用。
/* main.c */
#include <stdio.h>
#include "stack.h"
int main(void)
{
push('a');
push('b');
push('c');
while(!is_empty())
putchar(pop());
putchar('\n');
return 0;
}
- 对于用角括号包含的头文件,gcc首先查找-I选项指定的目录,然后查找系统的头文件目录(通常是/usr/include)。
- 对于用引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。
如果stack.h在stack文件夹下,并不在main.c同一级
.
|-- main.c
`-- stack
|-- stack.c
`-- stack.h
那么,需要用gcc -c main.c -Istack编译。
或者#include “stack/stack.h”,那么编译时就不需要加-Istack选项了
/* main.c */
#include "stack/stack.h"
Linux链接库(值得看看)
- 多个库文件链接顺序问题
参考这里,非常详细,值得看看。
库之间的依赖顺序,依赖其他库的库一定要放到被依赖库的前面
- 生成静态库/动态库
CAdd.h
int cadd(int x, int y);
CAdd.c 注意这里定义函数,没有main()
#include "CAdd.h"
#include <stdio.h>
int cadd(int x, int y) {
printf("from C function.\n");
return (x + y);
}
生成静态库
- 源文件中只提供可以重复使用的代码,例如函数、设计好的类等,不能包含 main 主函数;
- 源文件在实现具备模块功能的同时,还要提供访问它的接口,也就是包含各个功能模块声明部分的头文件
静态库文件其实就是对.o中间文件进行的封装,使用nm libadd.a命令可以查看其中封装的中间文件以及函数符号。 链接静态库就是链接静态库中的.o文件,这和直接编译多个文件再链接成可执行文件一样
gcc -c CAdd.c # 生成CAdd.o
ar -r libCAdd.a CAdd.o # ar压缩命令,归档生成libCAdd.a
调用静态库生成目标文件
gcc main.c libCAdd.a #和调用.o文件一样
生成动态库
gcc -shared -o libCAdd.so CAdd.c
调用动态库生成目标文件
#-L.指定搜索库文件路径,-llib搜索库文件
#优先链接so动态库,没有动态库再链接.a静态库
gcc main.c -L. -lCAdd
C/C++相互调用库的问题
C++调用C的函数比较简单,直接使用extern “C” {}告诉编译器用C的规则去调用C函数.
C语言没法直接调用C++的函数,但可以使用包裹函数来实现。
gcc 常用命令
常用的几个
参数 | 解释 |
---|---|
-c | 只编译,生成.o |
-o FILE | 生成目标文件FILE |
-IDIRECTORY | 指定额外的头文件搜索路径DIRECTORY |
-lxxx | 连接时搜索指定的函数库libxxx(库一般libxxx命名) |
-LDIRECTORY | 指定额外的函数库搜索路径DIRECTORY |
-Wall | 生成所有警告信息 |
总结
提示:这里对文章进行总结:
编译的过程分为词法解析和语法解析两个阶段,在词法解析阶段,编译器总是从前到后找最长的合法Token。把这个表达式从前到后解析,变量名a是一个Token,a后面有两个以上的+号,在C语言中一个+号是合法的Token(可以是加法运算符或正号),两个+号也是合法的Token(可以是自增运算符),根据最长匹配原则,编译器绝不会止步于一个+号,而一定会把两个+号当作一个Token。再往后解析仍然有两个以上的+号,所以又是一个++运算符。再往后解析只剩一个+号了,是加法运算符。再往后解析是变量名b。词法解析之后进入下一阶段语法解析,a是一个表达式,表达式++还是表达式,表达式再++还是表达式,表达式再+b还是表达式,语法上没有问题。最后编译器会做一些基本的语义分析,这时就有问题了,++运算符要求操作数能做左值,a能做左值所以a++没问题,但表达式a++的值只能做右值,不能再++了,所以最终编译器会报错。 ↩︎