前言:C语言是Java、Objective-C、C++等高级语言的基础、也是跨平台开发的基础,指针是C语言的重中之重,
&a
表示a变量所在地址,*p
表示指针p指向的地址的内容……这些常用的、常见的东西我们都比较清晰,这里就再整理一下C相关的注意点和一些技巧,当做知识点的巩固和完善吧。
C与Linux重温
一、C 编译过程
.c文件 -> (预处理) -> .i文件 -> (编译) -> .s文件 -> (汇编) -> .o文件 -> (链接) -> 可执行文件
-
$ gcc -o main.i main.c -E
或$ gcc -E main.c -o main.i
:表示只执行到预处理完成阶段,生成.i文件。这个过程,是一个处理过程,①展开头文件:将头文件中的内容,添加到源代码中,而不是以头文件的形式存在;②进行宏替换:单纯的字符串替换,预处理阶段,宏不会考虑c的语法,如下例子可以说明。
上述代码经过预处理之后,变成了:#include <stdio.h> #define R 10 #define M int main( M) { int a = R; //.... return 0; }
........ <stdio.h>文件所展开的内容,这里忽略 ........ int main() { int a = 10; ///R 被替换成了 10 //......... return 0; }
- 关于宏替换
- 特点:直接从代码上替换字符串,不考虑c的语法。
- 优势:可不考虑参数类型,如:
#define ADD(a,b) (a+b)
,我们可以使用ADD(1,2)
,也可以使用ADD(1.5,2.3)
。
- 关于typedef
- 与宏的区别:
- 宏替换在预处理过程中就执行,而typedef在预编译过程后的.i文件中,不会进行任何替换操作。
- 宏以下的所有代码,都可以使用到宏,而typedef的作用域有限,如果定义在方法体中,就只能在方法中生效。
二、Linux相关命令
- ls -l : 输出当前目录所有文件(名字或者权限等信息系)
- echo $? :查看上一条执行的语句的返回码:0表示执行成功
- cat filename: 读取文件filename的内容,并显示到终端
- gcc main.c && ./main.c : 这个
&&
表示,前一句执行成功,后一句才会开始执行
三、关于makefile文件
- 复习:关于
gcc
命令选项:-
gcc xxx -o filename
:表示将xxx文件经过处理,输出到名为filename的文件。(.m 结尾:Objective-C源码文件) -
gcc -E hello.c -o hello.i
:-E
表示只进行到‘预处理’阶段,生成.i结尾的预处理后的C源码文件。(.ii 结尾:预处理后的C++源码文件) -
gcc -S hello.c -o hello.s
:-S
表示只进行到‘汇编’阶段,生成.s结尾的汇编语言源代码文件。(-S 结尾:预处理后的汇编语言源码文件) -
gcc -c hello.c -o hello.o
:-c
表示只进行到‘编译’阶段,生成.o结尾的编译后的目标文件。 -
gcc hello.c -o hello
:直接生成可执行文件。 -
gcc -g hello.c -o hello
: 可执行文件中加入调试信息
-
- 意义:
将需要编译的多组.o和.c文件,他们的编译规则和编译顺序写好在一个文件中,这样就代替了繁杂的gcc
命令。 - 步骤
- 首先,在这堆c文件所在目录下,创建一个文件,名为Makefile
$ vi Makefile
- 然后,输入该文件的内容如下:
# this is make file main.out:lib1.o lib2.o main.c -o hello.out [两个Tab,表示8个空格]gcc lib1.o lib2.o main.c lib1.o:lib1.c [两个Tab,表示8个空格]gcc -c lib1.c lib2.o:lib2.c [两个Tab,表示8个空格]gcc -c lib2.c
- 保存文件,最后,在这个目录下,执行
make
命令:
make
四、关于C的main函数
///其中:argv表示执行时带有的参数个数,argc表示执行时所带参数列表。
int main(int argv, char* argc[]){
//doSomething;
return 0;///执行之后,返回值(0表示成功)
}
例如执行:./ main.out -a -l -d
,那么,argv=4,argc分别为:./main.out
,-a
,-l
,-d
。
五、C的标准输入输出流和错误流
我们知道,include <stdio.h>
的这个头文件,在我们执行应用程序的瞬间,操作系统为程序启动了一个进程,之后,当包含进这个头文件后,它会给我们提供一系列指针,指向资源,应用程序被启动时,他会为我们创建三个文件,分别是:stdin、stdout、stderr,分别对应于:标准输入、输出和错误流,负责为我们的应用程序提供输入和输出数据的能力。
- stdin:标准输出流,默认是我们的屏幕显示器终端
printf("hello\n");
底层是:fprintf(stdin, "hello\n");
- stdout:标准输入流,默认设备是我们的键盘
scanf("%d", &n);
底层是:fscanf(stdout, "%d", &n);
- stderr:标准错误流,报告程序出错时的输出操作:
if(……){ fprintf(stderr, "error!"); return 1;//这里很关键,返回不是0,表示程序执行有错误。 }
六、重定向机制
-
输出重定向
- 把程序的输出流,重定向到一个新的文件中,填充文件内容【不覆盖】:
./main.out >> output.txt //或者 ./main.out 1>> output.txt
- 把上一步执行的结果,重定向到一个文件中,覆盖文件内容:
ls /etc > etc.txt
- 标准输出流和标准错误流分别输出到不同文件,正常输出为true.txt ,错误输出为fail.txt:
main.c代码如下:
#Include<stdio.h> int main(){ int a,b; scanf("%d", &a); scanf("%d", &b); if(0!=b) printf("a/b=%d\n", a/b); else{ fprintf(stderr, "j!=0\n"); return 1; } return 0; }
命令行输入如下:
$ cc main.c -o main.out $ ./main 1>true.txt 2>fail.txt ////如果有输入的文件,还可以: // $ ./main 1>true.txt 2>fail.txt <input.txt
-
输入重定向
- 将要输入的参数值,写入一个文件input.txt中,然后代替键盘键入:
./main.out < input.txt
七、结构体相关
- 结构体定义与数组赋值
struct person{
char name[20];
int age;
};////可以在“;”号前直接定义一个全局变量为me
.......
int main()
{
struct person team[2] = {"xiao_ming", 20, "hua_zai",18};
//或者:struct person team[2] = {{"xiao_ming", 20}, {"hua_zai",18}};
}
- 结构体指针
- 指向单个结构体对象
struct person xiaoming = {"xiaoming", 19};
struct person * p1;
p1 = &xiaoming;
printf("name=%s\n", (*p1).name);///也可以写成:p1->name 或者 xiaoming.name ,这三种方法是等价的。
- 指向结构体数组对象
struct person team[2] = {{"xiao_ming", 20}, {"hua_zai",18}};
struct person * p;
p = team; //没有了‘&’
printf("name=%s\n", p->name);///相当于 team[0].name
printf("name=%s\n", (++p)->name);///相当于 team[1].name
- 结构体的大小【重要】
<u>结构体大小 = 结构体最后一个成员的偏移量 + 最后一个成员的大小 + 末尾的填充字节数</u>
例子:
struct data{
int a;///偏移量为0
char b;///偏移量为4【因为偏移量‘4’是b本身大小‘1’的整数倍,所以,编译器不会在成员a和b之间填充字节】
int c;///偏移量为5 --> 8【编译器在b后面填充了字节,本来是5,最后变成8,因为8才是4的整数倍】
///这样一来,整个data结构体的大小是 4+4(1+3)+4=12 ,而判断得出:12%4(最宽的基本类型int的大小) == 0,所以,最终大小不会再被编译器填充字节,就是12。
}
八、联合体
- 联合体的定义
union data{ ///联合体 data,它所占的内存空间为最大元素所占的空间,所以,为4byte。
int a;
char b;
int c;
}
九、动态链表与静态链表
- 两者都是动态数据结构
///链表结点结构体
struct node{
int data;
node * next;
};
- 两者的区别
- 静态链表写法
main方法中:
///main方法中
struct node a,b,c, * head;
a.data = 1;
b.data = 2;
c.data = 3;
a.next = b;
b.next = c;
c.next = NULL;
head = &a;
///以head为头结点指针的静态链表创建成功
///...............
struct node *p = head;
while(p->NULL){
printf("%d\n", p->data);
p = p->next;
}
- 动态链表写法
include <stdio.h>
include <stdlib.h>//当中有malloc相关api
///链表结点结构体
struct node{
int data;
node * next;
};
///创建链表的函数
struct node * create(){
struct node head;
struct node p1,p2;
int n = 0;
p1 = p2 = (struct node)malloc(sizeof(struct node));
scanf("%d", &p1->data);
head = NULL;
while(p1->data!=0){
n++;
if(n==1) head = p1;
else p2->next = p1;
p2 = p1;
p1 = (struct node*)malloc(sizeof(struct node));
scanf("%d", &p1->data);
}
p2 ->next = NULL;
return (head);
}
///main方法中
int main(){
struct node * p;
p =create();
printf("第一个结点的信息:%d\n", p->data);
return 0;
}
```
十、C语言的变量存储类别
- 根据变量的生命周期来划分,分为:静态存储方式和动态存储方式
- 静态存储方式:指在程序运行期间分配固定的存储空间的方式。存放了在整个程序执行过程中都存在的变量(如:全局变量)
- 动态存储方式:在程序运行期间更具需要进行动态的分配存储空间的方式。动态存储区中存放的变量是根据程序运行的需要而建立和释放的。(如:函数形参、自动变量、函数调用时的现场保护和返回地址等)
- C语言中的存储类别:自动(auto)、静态(static)、寄存器(register)和外部(extern)
自动(auto): 用关键字auto定义的变量为自动变量,auto可以省略,auto不写则隐含定为“自动存储类别”,属于动态存储方式。如:
auto int a;
相当于int a;
-
静态(static): 用static修饰的为静态变量,如果定义在函数内部的,称之为静态局部变量;如果定义在函数外部,称之为静态外部变量。如下为静态局部变量。
int func(){ static int a = 1; a++; printf("a = %d\n", a); } int main(){ int i = 0; for(;i<10;i++){ func(); }///最终得到:a = 1,a = 2,..., a = 10 }
注意:静态局部变量属于静态存储类别,在静态存储区内分配存储单元,在程序整个运行期间都不释放;静态局部变量在编译时赋初值,即只赋初值一次;如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符变量)。
-
寄存器(register): 为了提高效率,C语言允许将局部变量得值放在CPU中的寄存器中,这种变量叫“寄存器变量”,用关键字register作声明。如
register int a = 1;
注意:只有局部自动变量和形参可以作为寄存器变量;一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量;局部静态变量不能定义为寄存器变量。
-
外部(extern): 用extern声明的的变量是外部变量,外部变量的意义是某函数可以调用在该函数之后定义的变量。
int main(){ extern int b; printf("%d\n" , b);///实际上是调用main函数之后的代码中的全局变量b,结果为100. } int b = 100;
十一、C语言的内部函数与外部函数
-
内部函数:
- 定义:在C语言中不能被其他源文件调用的函数称谓**内部函数 **,内部函数由static关键字来定义,因此又被称谓静态函数。
- 形式:static [数据类型] 函数名([参数])
-
外部函数:
- 定义:能被其他源文件调用的函数称谓**外部函数 **,外部函数由extern关键字来定义。
- 形式:extern [数据类型] 函数名([参数])
C语言规定,在没有指定函数的作用范围时,系统会默认认为是外部函数,因此当需要定义外部函数时extern也可以省略C语言规定,在没有指定函数的作用范围时,系统会默认认为是外部函数,因此当需要定义外部函数时extern也可以省略。
Linux 操作系统下对于C语言的内存管理和分配
一、32位和64位操作系统
操作系统理论上会将我们安置的内存条的所有地址空间进行编号(从000……0 到 111……1),直到它所能区分的位置总数(比如:我插了两条4G内存条到64位OS下的电脑时,理论上,就能将整个内存区分成 2的33次方 个位置)
- 32位操作系统只能使用4G内存(由于CPU的地址总线为32位,对应的寻址空间大小为2的32次方,也就是说:“我最多区分出 2的32次方 个不同的位置”)
- 64位操作系统可以使用足够大的内存
二、用户内存隔离
- 首先, 内存管理和分配是由操作系统来帮我们完成的。
- 64位操作系统中,对于内存分配:
- 0xffffffffffffffff ~ 0x8000000000000000:系统程序使用内存【前16位】
- 0x7fffffffffffffff ~ 0x00:用户程序使用内存【后48位】
- 用户内存分配结构:
- 代码段【内存中的最低段位】:代码编译后的二进制数据加载到内存中的最低位处,即:代码段
- 数据段【内存中的第二低段位】:声明一些全局变量、(全局或函数中的)静态变量或者常量,则放在了数据段处
- 堆【内存中的第三低段位】:
- 自由可分配内存【内存中的第四低段位】
- 栈【内存中的第二高段位,最高段位是系统内存】:记录函数当前运行时的状态,记录“代码运行到第几行,内部的变量所在内存地址等信息等(如:main函数开头连续声明两个int类型变量a和b,那么,a和b所在内存地址数之间差为4个字节)”。一个函数可能被多次调用,而每次调用函数,都是一个独立的栈;先声明的函数所处内存地址位置低,后声明的函数所处内存地址位置高,而系统对栈的地址分配则相反,先分配的栈所在地址更高(如:main方法调用方法A,那么对main方法分配的栈比对方法A分配的栈地址位置高,);
- gcc对内存分配的优化
- 首先,在程序代码编译后,gcc编译器会将零碎的声明的所有非静态变量,按照类型进行归类,同一类型的变量声明在一块,然后去为每一类的变量集合分配内存。这样一来,同一类型的变量实际在内存的栈空间中的位置是相邻的。
- 对C语言来说,32位OS中,指针变量占4byte;64位OS中,指针变量占8byte。
三、函数栈、函数指针
-
(一)函数栈
C中的函数调用,每一个函数的调用,系统都会为其分配一个栈,用于存放这个函数的信息(目前执行第几行、成员变量的信息等),这个就是函数栈。【注意:函数内部的静态变量位于内存中的数据段,而非栈区】 -
(二)函数指针
看个例子:
#include<stdio.h>
int func(int a){
return a*a;
}
int main(){
int b = 3;
int res;
res = (*func)(b);
printf("%d\n",res);
return 0;
}
这里的res = (*func)(b);
指的就是:调用func(3),并将返回值赋给res。其中,func
表示func函数所在代码段中的地址本身,(*func)
表示找到这个func名对象对应的代码段中此函数打包块,相当于获取到整个函数。
四、指针运算
-
(一)高效率的指针偏移
前面说过,由于gcc对变量指向内存地址的优化,同一类对象会被归在一段连续分配的内存中。为什么说“高效率”?因为每次指针偏移“1”,就能准确地定位到原地址的下一个对象存储的首地址,此过程是根据类型大小而适配的,相对于for
循环,指针偏移只需要内部执行一条偏移语句即可,所以会“更高效率”。
下面看看这个示例:#include<stdio.h> int main(){ int a = 3; int b = 2; int arr[3]; int *p = &a; arr[0] = 1; arr[1] = 10; arr[2] = 100; p+=3;///注意点一 *p = 4; p = &a; int i;///注意点二 for(i=0;i<6;i++){ printf("*p = %d , p[%d] = %d\n", *(p+i), i, p[i]);///注意点三 } return 0; }
打印结果为:
*p = 3 , p[0] = 3 *p = 1 , p[1] = 1 //注意点四 *p = 2 , p[2] = 2 *p = 4 , p[3] = 4 *p = 10 , p[4] = 10 *p = 100 , p[5] = 100
以上的示例代码中,有三个注意点,用注释标记出来:
- 注意点一:
p+=3; *p=4;
等价于:①p[3]=4
②*(p+3) = 4
,相当于让p所指的地址之后的第三个地址赋上4。 - 注意点二和注意点四:这里就可以结合以上说的 ‘gcc对变量和内存地址指向的优化’ 来解释,gcc将同一类型的变量放在连续分配的内存空间中排列,于是,当p指向a的地址,a地址和b地址之间,还存在变量i所在的地址(由于gcc的优化整理),所以,这里才有了
p[1]=1;
- 注意点三:这里可以得出,指针运算中,
*(p+n)
和p[n]
效果是一样的。
注意:这里专指Linux64位系统下gcc对C语言的相关优化支持,在MacOS或其他系统下,可能会有一定差异。
- 注意点一:
(二)字符数组和指针字符串
看看一下示例:
#include<stdio.h>
int main(){
char s[] = "hello";
char *s2 = "world";
char s3[10];
scanf("%s", s3);
printf("%s, %s ,%s\n", s,s2,s3);
}
注意点:
- 首先,这段代码,三个变量s、s1、s2都是可以正常输出的。
- s[] 被当成一个值,而 s2 被当成一个指针,指向这个“world”的首地址。
- 此时,可以看出,指针和字符数组可以适当混用,在输入s3时,可知,s3本身指代一个内存地址,所以不用加“&”符号【 s 同理】。
- 但是,如果是要输入到s2中,就不行了。因为C语言中,字符数组的大小等于字符数加上“\0”,这个“\0”是结束符号(比如上述的
s[]
的大小就是6个字节),而*s2
本身是指针类型,指向的是内存的代码段中的“world”(由gcc编译时决定的,会认为s2
指向的是"world"字符串常量),而不是栈空间,而代码段中的成员是没有修改权限的,栈和堆中的对象才可以修改。 -
溢出的情况:如果是
scanf("%s" , s);
,那么如果输入超过6个字符,那么,原来s
的末尾的\0
结束符将被覆盖,而上述示例中,由于gcc优化,s
和s3
的内存地址实际上是相邻的,s
原始占有6个字节,而s3
原始占有10个字节,那么,如果输入大串字符,则会覆盖s
甚至s3
的原来的内容甚至其他内存空间的内容,这样一来,就会造成很严重的后果!!