C语言学习中容易疏忽的问题

文章目录

最近经常用到C语言,发现尽管大学时已经学过了,但是真正用起来还是有很多不足的地方,在这里权且总结一下。

c语言编译过程

编译的完整过程大概为以下流程:

C源程序-->预编译处理(.c)-->编译、优化程序(.s、.asm)-->汇编程序(.obj、.o、.a、.ko)-->链接程序(.exe、.elf、.axf等)。

预编译处理(.c)

它主要包括四个过程
a、宏定义指令,如#define N 6,#undef等。
对于前一个伪指令,预编译所要做的是将程序中的所有N用6替换,请大家注意这里是替换,并不是像作为函数参数那样将6复制进N这个变量。对于后者,则将取消对某个宏的定义,使以后出现的N不再被替换。

b、条件编译指令,如#ifdef,#ifndef,#endif等。
这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。这样就能在编译阶段减少编译时间,提高效率。

c、 头文件包含指令,如#include “file.h”或#include 等。
在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。
采用这样的做法一来可以让我们直接调用一些复杂库函数;二来可以免去我们在写程序时重复做一些定义声明工作的麻烦。

这里顺便提一下#include<>与#include“ ”的区别。
•#include<>:这条指令就是告诉编译器去系统默认的路径寻找相关文件。
•#include” ”:这条是告诉编译器先去源程序所在目录下寻找,如果没有就去系统默认路径寻找。

d、特殊符号,预编译程序可以识别一些特殊的符号。
例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序就是对在源程序中出现的这些特殊符号将用合适的值进行替换。

总结:预编译阶段基本上是完成对源程序的相关代码进行替换,这样之后程序的原意没有改变,就是代码的内容有所不同,这样为以后的编译做好准备。

编译、优化程序(.s、.asm)

经过上一阶段的处理,现在我们的程序已经没有宏定义,包含头文件等指令了,只剩下一些变量,常量,关键字等,而编译的主要作用是检查这些代码的语法错误及将这些代码编译成为汇编文件。
优化程序很复杂,不仅和编译技术本身有关,还和目标板相应的硬件环境有很大的关系。

汇编程序(.obj、.o、.a、.ko)

在这个阶段是将汇编代码翻译成目标文件,这时的文件已经是二进制代码了。在windows环境下文件的后缀名是.obj;而在unix下则有是o、.a、.ko等文件。
目标文件由段组成。通常一个目标文件中至少有两个段:

•代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。

•数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

链接程序(.exe、.elf、.axf)

也许有人会有疑问,上面的目标代码已经是机器码了,也就是说CPU可以识别这些文件了,那为什么我们还要链接程序呢?大家想想我们是不是忘了点什么。。。对!那些被包含的头文件,以及当我们的程序分布于很多源文件时,那么这些源文件该怎么处理呢,这就是连接器的作用,它们被翻译成目标代码后需要被链接到一起才能被执行。

谈到函数库的链接,我们还需要了解点函数库的知识,函数库分静态链接库(又称静态库*.lib)和链接动态库(又称动态库*.dll)。
静态库的链接在编译时会被编译进汇编文件,这样的操作会改变文件大小;而动态库则是在执行时(双击运行),当需要动态库中的文件时才被链接到可执行文件的。

C语言中内存分配问题

C语言中内存分为五个区:

•栈(stack):用来存放函数的形参和函数内的局部变量。由编译器分配空间,在函数执行完后由编译器自动释放。

•堆(heap):用来存放由动态分配函数(如malloc)分配的空间。是由程序员自己手动分配的,并且必须由程序员使用free释放。如果忘记用free释放,会导致所分配的空间一直占着不放,导致内存泄露。

•全局区/静态区:用来存放全局变量和静态变量。程序结束时由系统释放,分为全局初始化区和全局未初始化区;存在于程序的整个运行期间,是由编译器分配和释放的。

•文字常量区:常量字符串放于此,程序结束时由系统释放。例如char *c = “123456”;则”123456”为文字常量,存放于文字常量区。也由编译器控制分配和释放。

•程序代码区:用来存放程序的二进制代码。

下面通过几个例子来看一下,具体C程序中的内存分配吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 例1    main1.c
 * 演示C程序中的内存分配方式。
*/
#include <stdlib.h>
#include <string.h>
 
int a = 0; //全局初始化区
char *p1; //全局未初始化区
 
int main()
{
    int b; //栈
    char s[] = "abc"; //s在栈,"abc\0"在文字常量区
    char *p2; //栈
    char *p3 = "123456"; //"123456\0"在常量区,p3在栈上
    static int c =0; //全局区
    p1 = (char *)malloc(10); //p1在栈,分配的10字节在堆
    p2 = (char *)malloc(20); //p2在栈,分配的20字节在堆
    strcpy(p1, "123456"); //"123456"放在常量区,编译器可能会优化为和p3的指向同一块区域
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 例2 main2.c
* 演示C语言中的内存分配。
*/
 
#include <stdio.h>
 
//返回char型指针
char *f()
{
    //s数组存放于栈上
    char s[4] = {'1','2','3','0'};
    return s; //返回s数组的地址,但程序运行完s数组就被释放了
}
 
int main()
{
    char *s;
    s = f();
    printf("%s", s); //打印出来乱码。因为s所指向地址已经没有数据
    return 0;
}

下面对两段代码进行一下分析:

1
2
3
4
5
6
7
8
9
10
char* toStr()   
{  
    char *s = "abcdefghijkl";  
    return s;  
}  
int main()  
{  
    cout << toStr() << endl;  
    return 0;  
}

1
2
3
4
5
6
7
8
9
10
char* toStr()   
{  
    char s[] = "abcdefghijkl";  
    return s;  
}  
int main()  
{  
    cout << toStr() << endl;  
    return 0;  
}

前一段代码打印出来是字符串,而后一段代码打印出来就是乱码。记得学C语言的时候讲到,字符串是被当做字符数组来处理的。所以字符数组名就相当于指向首地址的指针。那么
1. char *s = “abcdefghijkl”;
2. char s[] = “abcdefghijkl”;
这两种表达式似乎是一样的,可是为什么程序结果会不一样呢?原因就是没有对内存分配了解好。当然现在的C语言教材不会讲到的。
解释:
程序的意思比较简单,不用解释。
•第一种表达式,指针s是局部变量,他的作用域是函数toStr内。它将其指向的地址返回,返回之后s即被销毁,庆幸s指向的地址被返回了回来。最终打印正确。

•第二种表达式,那么我们会问第二种与第一种的区别在哪,为何错?原因就是第一种指针s虽然是局部变量,被分配在栈空间,作用域是函数内部,但其指向的内容”abcdefghijkl”是常量,被分配在程序的常量区。直到整个程序结束才被销毁。而第二种,s是一数组,分配到栈空间,”abcdefghijkl”作为数组各个元素被放到数组中,一旦函数退出,栈中这块内存就被释放。虽然返回一个地址,可是已经失去它的意义了。

变量初始化

变量初始化不是在编译阶段完成的(静态存储变量和外部变量的初始化是在编译阶段完成的),而是在程序运行时执行本函数时赋初值的。
因此:

1
2
3
4
5
6
#include<stdio.h>
main()
{
   int a=sizeof(int);
   printf("%d",a);
}

sizeof是在预编译期间完成的,是一个关键字,输出的结果应该为4。

指针变量赋初值,必须是已经开辟好了空间,比如char * s=“hello”;这个字符串,就是个常量,会在静态存储区分配个空间,s里面装的就是它的起始地址。
而int *a=12;把0×00000012这个地址给指针(它只是在编译器中的一个数,不是我们说的常量)。并没有给这个地址分配空间,所以会出错。

static及const关键字的使用

在C语言中,关键字static有三个明显的作用:
•在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
•在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
•在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

在C语言中,关键字const的作用为:
修饰变量表示变量的值不可被修改;
修饰指针,如果const在*左边,表示指针指向的值不可以修改。const在*右边,表示指针值(也就是指针指向的位置)不可以修改。
修饰函数中的参数,表示该函数参数不可被改变。

位运算相关操作

C语言提供的位运算符列表:
& 按位与 如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
| 按位或 两个相应的二进制位中只要有一个为1,该位的结果值为1
^ 按位异或 若参加运算的两个二进制位值相同则为0,否则为1
~ 取反 ~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1变0
<< 左移 用来将一个数的各二进制位全部左移N位,右补0
>> 右移 将一个数的各二进制位右移N位,移到右端的低位被舍弃,对于无符号数,高位补0

重点讲解一下^、<<及>>的用法。

交换两个值,不用临时变量(设 a=a1,b=b1)

1
2
3
4
  操作            目标                操作后状态
 a=a^b           a=a1^b1            a=a1^b1,b=b1
 b=a^b           b=a1^b1^b1         a=a1^b1,b=a1
 a=a^b           a=b1^a1^a1         a=b1,b=a1

该用法对于数组的反转,字符串的反转,及排序等算法具有重要的意义,可以节省内存空间,降低空间复杂度。

判断int型变量a是奇数还是偶数

1
2
a&1   = 0 偶数
a&1   = 1 奇数

int型变量循环左移k次

1
a=a<<k|a>>16-k   (sizeof(int)=16)

int型变量循环右移k次

1
a=a>>k|a<<16-k   (sizeof(int)=16)

#if,#if defined ,#ifdef

#ifdef命令最常见的形式为:

1
2
3
4
5
#ifdef 标识符 
    程序段1 
#else 
    程序段2 
#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。

而#if命令的用法一般为

1
2
3
4
5
#if 表达式 
    程序段1 
#else 
    程序段2 
#endif

它的作用是:当指定的表达式值为真(非零)时就编译程序段1,否则编译程序段2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。

for循环执行流程

通过一个例子我们来学习一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void main( )
{
  int i = 0, j = 10;
    for(i=0;i<j;i++,j--)
    {
        if(j==10)
        {
            printf("%d,%d ",i,j);
            j--;
        }
        j++;
    }
    printf("%d,%d ",i,j);
}
 
  a) 0,10
    9,9
 
  b) 0,10
    10,10
 
  c) 10,10
 
  d) 9,11

c语言中for循环的执行方式应该是
1)循环开始时先执行初始化语句,即i=0;
2)每次循环前执行判断语句:i 3)每次循环后执行动作语句:i++,j–。

那么根据这个原则,我们可以看出,应该选择A。

本文出自 TENNFY博客,转载时请注明出处及相应链接。

本文永久链接: https://www.tennfy.com/2618.html

下一篇文章:

上一篇文章:

3人参与了讨论

  1. xilouqingzhu 说:

    我勒个擦,又开始学c 了呀

  2. 给力啊。,,C好久没看了

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

*

4 + 5 = ?


您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

返回顶部