C知识点

C语言的产生

1969年,通用电气、麻省理工学院和贝尔实验室联合创立了一个multics项目,这个项目的目的是创建一个操作系统。经过一段时间之后,终于失败了,其中一个叫做Thompson的研究员编写了一个简易的新型操作系统,他比multics简单的多(即UNIX),整个系统由汇编语言编写的。虽然成功了,但是在编制数据结构的时候浪费了大量的时间,而且系统难以调试,理解起来也有困难。于是他想使用高级语言进行编写,因此Thompson创建了B语言,其实就是简化了下BCPL语言,使得B的解释器能够常驻于PDP-7只有8k的内存中,B语言不曾成功过,因为硬件系统的内存限制,只允许存放解释器,而不是编译器,因此产生的低效阻碍了使用B语言进行系统编程。因为B语言是一个无类型语言,因此当将开发平台移植到PDP-11以后,这种语言就显得不是很合适了(PDP-11支持集中不同长度的数据类型)。Dennis Ritchie利用PDP-11的强大性能创建了能够同时解决多种数据类型和效率低下问题的new B语言(随后改名为C)。它采用编译的方式执行而不时解释的方式并引入了类型系统。

多做之过

多做之过,就是语言中某些不应该存在的特性,包括容易出错的switch语句,相邻字符串常量的自动连接和缺省全局作用域

switch

  1. default可以出现在case列表的任何位置,它在其他的case均无法匹配时被选中执行,如果没有default,而且所有的case均不能匹配的时候,那么整条switch就什么也不做,default和case的顺序可以是任意的,但习惯上将default放在最后
  2. 可以在switch的左花括号之后声明一些变量,从而进行一些局部存储的分配,不要企图去初始化,因为执行就就没有到这里,语句从匹配表达式的case处开始
  3. switch内部的任何语句都可以加上标签,并且在执行的时候跳转到那里
  4. switch不会在每个从case标签后面的语句执行完毕后自动中止,除非遇到break。如果是程序员精心设计的,那么加上/* fall through */是最好的

字符串的连接问题

  1. ANSI C中相邻字符串常量将被自动合成一个字符串的约定,这就省掉了过去在书写多行信息时必须在末尾加上\的做法,后续的字符串可以出现在每行的开头
  2. 他们会在编译时自动合并,除了最后那个字符串以外,其余每个字符串末尾\0都会被删除

全局函数问题

  • 定义C函数时,在缺省情况下函数的名字是全局可见的,可以在函数的名字前加一个多余的extern关键字,也可以不加,效果是一样的。
  • 如果想要限制对于这个函数的访问,必须加上一个static关键字
  • 建议在函数前加上static,达到控制可见性的目的
  • C语言中的符号要么对于全局可见,要么对于其他文件都不可见

误做之过

误做之过,就是语言中有误导性质或者不适当的特性,这些特性有的和C语言的简洁性有关,有的则与操作符的优先级有关

  • 当sizeof的操作数是一个类型名时,两边必须加上括号(虽然加上括号,但是注意他是一个操作符),但是操作数如果是变量则不必
  • 在优先级组成的意群的地方,意群内部的计算次序始终是未定义的。之所以未定义是想让编译器充分利用自身架构的特点,或者充分利用存储于寄存器中的值

可能出现错误的优先级

什么是结合性?

他是仲裁者,在几个操作符具有相同的优先级的时候决定先执行哪一个。同一个优先级的操作符,他们的结合性也一样,这样才能保证无歧义

少做之过

少做之过,语言应该提供但未能提供的特性。

  • 空格是可以改变程序的意思或者程序有效性的。我们使用\转义回车的时候,如果在其中加上空格,结果就不一样了。
  • ANSI C规定一为人所知的最大一口策略,这个策略表示,如果下一个班级有从超过一种的解释方案是,编译器将选取能组成最长字符序列的方案 z = y++ +x

指针常量和常量指针

最后一种情况下,指针是只读的,而在另外两种情况下,指针所指向的对象是只读的
const int *grape;
int const *grape;
int *const grape;

结构体

结构的内容可以是任何其他数据声明,单个数据项、数组、其他结构、指针等。另外还需要注意的一点是,可以在struct关键字后面加一个可选的“结构标签”

结构中也允许存在位段、无名字段以及字对其所需的填充字段。这些都是通过在字段的声明后面加一个谋好以及一个表示字段位长的整数来实现的。位段的类型必须是int,unsigned int或signed int。

结构体变量的声明应该与类型的声明分开。

参数在传递是首先尽可能地存放到寄存器中,注意,int类型变量i和只包含一个int型成员的结构变量s在参数传递时的方式可能完全不同。一个int型参数一般会被传递到寄存器中,而结构参数则很可能被传递到堆栈中。

在结构体中防止数组,可以把数组当做第一等级的类型,用赋值语句拷贝整个数组,以值传递调用的方式把它传递到函数或者把它作为函数的返回类型

联合体

在结构中,每个成员依次存储,而在联合中,所有的成员都从偏移地址0开始存储,这样每个成员都重叠在一起,某一时刻,只有一个成员真正存储于该地址。

  1. 联合一般用来节省空间,因为有些数据项是不可能同时出现的,如果同时存储他们,有些浪费
  2. 联合可以把同一个数据解释成多种类型。

枚举

把一串名字与一个整数值联系在一起
缺省情况下,整数值从零开始,如果对列表中的某个标识符进行了赋值,那么紧接其后的那个标识符的值就比所赋的值大1,然后类推。
枚举的优点:#define定义的名字一般在编译时被丢弃,而枚举名字通常一直在调试器中可见,可以在调试代码时使用它们。

理解C语言声明的优先级规则

1)声明从他的名字开始读取,然后按照优先级顺序依次读取
2)优先级从高到低依次是
	a)声明中被括号括起来的那部分
	b)后缀操作符
	c)前缀操作符
3)如果const和volatile关键字后面紧跟类型说明符(如int long等),那么它作用于类型说明符,在其他情况下,const和volatile关键字作用于它左边紧邻的指针星号

typedef

typedef为一种类型引入新的名字,而不是为变量分配空间,他并没有引入新类型,而是为现有类型取个新名字。
typedef为数据类型创建别名,而不是创建新的数据类型,可以对任何类型进行typedef声明
应该只对所希望的变量类型进行typedef声明,为变量类型取一个喜欢的别名,关键字typedef应该如前所述出现在声明的开始位置,在同一个代码块中,typedef引入的名字不能与其他标识符同名

我们应该把typedef看做是一个彻底封装的类型,即在声明它之后不能再往里面增加别的东西

typedef与宏定义的区别

1)宏可以用其他类型说明符对宏类型名进行扩展,而typedef所定义的类型名却不能这样做
	#define peach int
	unsigned peach i; // 没问题
	typedef int banana;
	unsigned banana i;//错误
2)在连续几个变量的声明中,用typedef定义的类型能够保证声明的所有变量均为同一种类型,而用define定义的类型则无法保证
	#define int_ptr int *
	int_ptr chalk, cheese; //第一个为指针,第二个为int
	
	typedef char * char_ptr;
	char_ptr bentley, rolls;//都是指针

C语言中的名字空间

1)标签名
2)标签:用于所有的结构、枚举和联合
3)成员名:每个结构或联合都有自身的名字空间

在同一个名字空间里,任何名字必须具有唯一性,但在不同的名字空间里可以存在相同的名字。

操作typedef的提示

1)不要为了方便起见对结构使用typedef,这样做的唯一好处就是你可以不必书写“struct”关键字,但这个关键字可以向你提示一些信息,你不应该把它扔掉
2)typedef应该用在
	a)数组、结构、指针以及函数的组合类型
	b)可移植类型。
	c)typedef也可以为后面的强制类型转换提供一个简单的名字

数组并非指针

C编程新手经常听到“数组和指针是相同的”,但是这一说法并不完全正确

以下代码有什么问题?
文件1 
	int mango[100];
文件2
	extern int *mango;

答案:对数组的引用总是可以写成对指针的引用,而且确实存在指针和数组定义完全相同的上下文环境,但是这只是一种最为普遍的用法,并非所有情况都是这样的。

什么是定义?什么是声明?

一个对象必须有一个定义,但是可以有多个extern声明
定义:定义相当于特殊的声明,它为对象分配内存
声明:声明相当于普通的声明,它所说明的并非自身,而是描述其他地方创建的对象

extern对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行,由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息,对于多维数组,需要提供出最左边一维之外的其他维的长度,这就给编译器足够的信息产生相应的代码

在C语言中,用变量名同时代表了变量地址和变量的内容,由编译器根据上下文环境判断他的具体含义。赋值左边为地址(左值,左值在编译时可知,左值表示存储结果的地方),赋值右边为内容(右值,右值直到运行时才知道,如果没有特殊说明,右值为变量内容)

编译器为每个变量分配一个地址,这个地址在编译时可知,而且该变量的内容在运行时一直保存于这个地址,相反,存储于变量中的值(他的右值)只有在运行时才可知,如果需要用到变量中存储的值,编译器就发出指令从指定地址读入变量值并将它存到寄存器中

对于每个符号的地址在编译时就是可知的,所以如果编译器需要一个地址(可能还会加上偏移)来执行某个操作,它就可以直接进行操作,并不需要增加指令来得到具体的地址,相反对于指针而言,必须首先在运行时取得它的当前值,然后才能对他进行解引用操作。

为什么extern char a[] 和extern char c[100]是等价的?

因为这两个声明都提示a是一个数组,即内存地址,数组内的字符可以从这个地址找到,编译器并不需要知道数组总共多长,因为它只产生偏移地址,从数组中提取一个字符,只需要将地址加上偏移,所需要的字符就在其中。

使用指针进行访问的过程

		1.取得符号表中p的地址,提取存储于此处的指针
		2.把下标和指针的值相加,产生一个地址
		3.访问上面的地址。

只要把p声明为指针,那么不管p原先是指针还是数组,都会按照上面所示的三个步骤进行操作,但是只有当p原来定义为指针的时候才是正确的。

数组中保存的是数据,而指针保存数据的地址。
在ANSI C中,初始化指针时所创建的字符串常量被定义为只读,如果试图通过指针修改这个字符串的值,程序就会产生未定义行为

编译器

绝大多数编译器并不是一个单一的庞大程序,他们通常由六七个程序所组成的,这些程序被一个叫做编译器驱动器的控制程序调用。这些小程序包括预处理器、语法和语义检查器、代码生成器、汇编器、优化器、连接器、当然还包括一个调用所有这些程序并向这些程序传递正确选项的驱动器程序。之所以把他们分成几个独立的程序,是因为这样设计可以使得代码更加容易维护和设计。

目标文件并不能直接执行,它首先需要载入到连接器中,连接器确认main函数为初始化进入点,把符号引用绑定到内存地址,把所有的目标文件集中在一起,再加上库文件,从而产生可执行文件。

静态链接和动态链接

如果函数库的一份拷贝 是可执行文件的物理组成部分,那么我们称之为静态链接,如果可执行文件中只是包含了文件名,让载入器在运行时能够寻找程序所需要的的函数库,那么我们称之为动态链接,
即便是静态链接中,也不是将整个库文件装入可执行文件,只装入需要使用的函数。

动态链接的优点

可执行文件体积很小,能够节省磁盘空间和虚拟内存。虽然运行速度慢一些,但链接-编辑阶段的事件缩短了。
使得函数库的版本升级更为容易,动态链接将程序与他们使用的特定的函数库版本分离开来,取而代之是一套接口,该接口保持稳定,不随事件和操作系统的后续版本发生变化
所有动态链接到某个特定函数库的可执行文件在运行时共享该函数的一个单独拷贝,操作系统内核保证映射到内存中的函数库可以被所有使用它们的进程共享。这样就提高了更好的IO和交换空间的效率,提高系统性能。

注:推荐只使用动态库。使用静态库可能因为操作系统的更新可能会与可执行文件绑定的系统函数库接口不兼容。

库的注意点

1)动态库文件的扩展名为.so,而静态库文件的扩展名为.a
2)我们在使用库的时候直接使用-l库的名字,即库文件去头去尾。
3)编译器在一些执行的目录去找库文件
4)动态库的符号提取是全部提取,静态库中符号提取是未定义符号提取。增加了静态库使用的风险。

函数库选项应该放在编译命令的最后面。

interpositioning问题

interpositioning问题就是通过编写与库函数同名的函数来取代该库函数的行为,这样可以使库函数在特定程序中被同名用户函数所取代,通常是用于调试或为了提高效率。这样做不仅仅是自己对所有同命库函数的调用将会被替换成自己版本的函数调用替代,而且所有调用该库函数的系统调用也将用该函数替代系统调用。
因为这并不是一个约束条件,所以当这种事情发生的时候,编译器并不会给出错误信息,只是会造成一些不可移植问题或者出现未定义的行为。

为什么编译器默认生成的可执行文件叫做a.out?

其全称是“assembler output”(汇编程序和链接编辑输出格式)的缩写形式,至于为什么叫做这个名字,是有其历史原因的,在PDP-7(那时候还没有B语言)上并不存在链接器,程序通常是先将所有源文件链接在一起,然后进行汇编,汇编产生的汇编程序输出保存在a.out中。等到以后使用链接器之后仍然保持了这个命名习惯。

段是二进程文件中简单的区域,里面保存了和某种特定类型相关的所有信息。section是ELF文件中的最小组织单位,一个段通常保存了几个section。
对于一个可执行文件执行size命令时,他会告诉你这个文件中文本段、数据段和bss段的大小。
对于可执行文件中的BSS段,我们可以把它认为是“batter save space”。由于BSS段只保存没有值的变量,所以实际上它并不需要保存这些变量的映像,只将运行时所需要的BSS段的大小记录在目标文件中,但BSS段并不占据目标文件的任何空间。

数据段保存在目标文件中
BSS段不保存在目标文件中
文本段最容易受优化措施影响的段
a.out文件的大小受调试状态下编译的影响,但段不受影响。

为什么以段的形式组织可执行文件?

因为段可以方便地映射到连接器运行时直接载入的对象中,载入器只取文件中每个段的映像。并直接将他们放入内存中。从本质上将,段在正在执行的程序中是一块内存区域。

文本段包含程序的指令,链接器把指令直接从文件拷贝到内存中(一般使用mmap系统调用)

数据段包含经过初始化的全局和静态变量以及他们的值。BSS段的大小从可执行文件中得到,然后链接器去获取该大小的内存并紧跟在数据段之后。该段会被初始化为0。包括数据段和BSS段的整个区域此时通常统称为数据区。一般情况下,在任何进程中的数据段是最大的段。

堆栈区用于保存局部变量、临时数据、传递到函数中的参数等。

堆空间:用于动态内存分配。

虚拟地址空间的最低部分是没有映射的,即它位于进程的地址空间,但是并未赋予物理空间,所以对于它的引用都是非法的,典型情况下,他是从地址0开始的几K字节,它用于捕捉使用空指针和小整型值的指针引用内存的情况。

为何我们可以访问调用栈中的其他局部变量

对于以下函数

void test(int *a)
{
	*a = 2;
}
int main()
{
	int a = I;
	test(&a);
	return 0;
}

我们知道函数调用是在栈中进行的,我们都知道栈是先进后出的一个结构。堆栈的典型定义是在堆栈中可以防止任意多的数据,但是唯一有效的操作就是从顶部取走一个数据或者是再次压入一个数据。在编译器的设计中采用了一种稍微灵活的做法,那就是除了以上的操作方法外,还允许我们访问位于堆栈中间的数据的值。体现在C语言上就是允许我们通过参数或全局指针访问调用它的函数的局部变量。

堆栈的用途

1) 堆栈为函数内部声明的局部变量提供存储空间
2) 进行函数调用时,堆栈存储与此有关的维护性信息(堆栈结构或者过程活动记录),其中包括了函数调用地址,任何不适合通过寄存器传递的参数以及一些寄存器值的保存(保护现场)
3) 堆栈也可以被用作临时存储区,可以使用alloca系统调用来分配堆栈中的空间。

过程调用记录

C语言自动提供的服务之一就是跟踪调用链—哪些函数调用了那些函数,当下一个return语句执行后,控制将返回何处等。解决这个问题的经典机制是堆栈中的过程调用记录。当每个函数被调用时,都会产生一个过程活动记录。过程活动记录是一种数据结构,用于支持过程调用,并记录调用结束以后返回调用点所需要的全部信息。

如何解释不能返回局部变量的地址

当函数结束后,变量就不复存在,它所占用的堆栈空间就被回收,可能在任何时候被覆盖,这样,指针就失去了有效性。被称为悬垂指针,他们并不引用有用的东西,而是悬在进程地址空间中。如果想反悔一个指向在函数内部定义的变量的指针时,要把那个变量声明为staitic,这样就能保证该变量被保存在数据段中而不是堆栈中,该变量的生命周期和程序一样,当定义该变量的函数退出之后,该变量的值依然有效。

为什么不用auto?

存储类型说明符auto关键字在实际中用不到,它通常是编译器设计者使用,用于标记符号表的条目-他表示在进入该块后,自动分配存储空间。对于其他程序员来说,auto关键字几乎没什么用处,因为它只能用于函数内部。但是在函数内部声明的数据缺省就是这种分配,使用auto唯一的好处就是代码看起来更加整齐。

证明了栈是向上生长的

void test02(int a, int b, int c)
{
	printf("a, %p\n", &a);
	printf("b, %p\n", &b);
	printf("c, %p\n", &c);
}

数组成员的访问格式

void test03(void)
{
	char buf[] = "hello";
	int i;
	for (i = 0; buf[i] != '\0'; i++)
	{
		printf("\n%c%c%c%c", buf[i], *(buf + i), *(i + buf), i[buf]);
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值