大牛查漏补缺 -- C语言注意点

前言: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;
    }
    
  • 关于宏替换
    1. 特点:直接从代码上替换字符串,不考虑c的语法。
    2. 优势:可不考虑参数类型,如:#define ADD(a,b) (a+b),我们可以使用 ADD(1,2),也可以使用ADD(1.5,2.3)
  • 关于typedef
    1. 与宏的区别:
    2. 宏替换在预处理过程中就执行,而typedef在预编译过程后的.i文件中,不会进行任何替换操作。
    3. 宏以下的所有代码,都可以使用到宏,而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命令。
  • 步骤
    1. 首先,在这堆c文件所在目录下,创建一个文件,名为Makefile
    $ vi Makefile
    
    1. 然后,输入该文件的内容如下:
    # 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
    
    1. 保存文件,最后,在这个目录下,执行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,表示程序执行有错误。
    }
    

六、重定向机制

  • 输出重定向

    1. 把程序的输出流,重定向到一个新的文件中,填充文件内容【不覆盖】:
    ./main.out >> output.txt
    //或者
    ./main.out  1>> output.txt
    
    1. 把上一步执行的结果,重定向到一个文件中,覆盖文件内容:
    ls /etc > etc.txt
    
    1. 标准输出流和标准错误流分别输出到不同文件,正常输出为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
    
  • 输入重定向

    1. 将要输入的参数值,写入一个文件input.txt中,然后代替键盘键入:
    ./main.out < input.txt
    

七、结构体相关

  1. 结构体定义与数组赋值
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}};
}
  1. 结构体指针
  • 指向单个结构体对象
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
  1. 结构体的大小【重要】
    <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。
}

八、联合体

  1. 联合体的定义
union data{ ///联合体 data,它所占的内存空间为最大元素所占的空间,所以,为4byte。
  int a;
  char b;
  int c;
}

九、动态链表与静态链表

  1. 两者都是动态数据结构
///链表结点结构体
struct node{
  int data;
  node * next;
};
  1. 两者的区别
  • 静态链表写法
    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位】
  • 用户内存分配结构:
    1. 代码段【内存中的最低段位】:代码编译后的二进制数据加载到内存中的最低位处,即:代码段
    2. 数据段【内存中的第二低段位】:声明一些全局变量(全局或函数中的)静态变量或者常量,则放在了数据段处
    3. 堆【内存中的第三低段位】
    4. 自由可分配内存【内存中的第四低段位】
    5. 栈【内存中的第二高段位,最高段位是系统内存】:记录函数当前运行时的状态,记录“代码运行到第几行,内部的变量所在内存地址等信息等(如: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
    

    以上的示例代码中,有三个注意点,用注释标记出来:

    1. 注意点一:p+=3; *p=4;等价于:①p[3]=4*(p+3) = 4,相当于让p所指的地址之后的第三个地址赋上4。
    2. 注意点二和注意点四:这里就可以结合以上说的 ‘gcc对变量和内存地址指向的优化’ 来解释,gcc将同一类型的变量放在连续分配的内存空间中排列,于是,当p指向a的地址,a地址和b地址之间,还存在变量i所在的地址(由于gcc的优化整理),所以,这里才有了p[1]=1;
    3. 注意点三:这里可以得出,指针运算中,*(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优化,ss3的内存地址实际上是相邻的,s原始占有6个字节,而s3原始占有10个字节,那么,如果输入大串字符,则会覆盖s甚至s3的原来的内容甚至其他内存空间的内容,这样一来,就会造成很严重的后果!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容