C/C++系列记录

C/C++系列记录(持续更新)

Linux 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缩写
countcnt
blockblk
lengthlen
messagemsg
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

调试的基本思想仍然是“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环,根据现象如何假设错误原因,以及如何设计新的现象去验证假设

  1. 单步执行和跟踪函数调用
  2. 断点
  3. 观察点
  4. 段错误

完整编译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

gcc命令这些选项都可以和-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生成所有警告信息

总结

提示:这里对文章进行总结:


  1. 编译的过程分为词法解析和语法解析两个阶段,在词法解析阶段,编译器总是从前到后找最长的合法Token。把这个表达式从前到后解析,变量名a是一个Token,a后面有两个以上的+号,在C语言中一个+号是合法的Token(可以是加法运算符或正号),两个+号也是合法的Token(可以是自增运算符),根据最长匹配原则,编译器绝不会止步于一个+号,而一定会把两个+号当作一个Token。再往后解析仍然有两个以上的+号,所以又是一个++运算符。再往后解析只剩一个+号了,是加法运算符。再往后解析是变量名b。词法解析之后进入下一阶段语法解析,a是一个表达式,表达式++还是表达式,表达式再++还是表达式,表达式再+b还是表达式,语法上没有问题。最后编译器会做一些基本的语义分析,这时就有问题了,++运算符要求操作数能做左值,a能做左值所以a++没问题,但表达式a++的值只能做右值,不能再++了,所以最终编译器会报错。 ↩︎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值