LinuxC语言编程01:复习C语言语法


这个系列的文章并不是C语言入门的文章,而是帮您入门如何在Linux环境下使用gcc编译器进行C语言开发.
在这里假定您曾经学习过一些在其他平台上的C或C++语言的基础(如VC++)或者任何其它编程语言并了解一些程序设计的基本概念.

复习C语言的基本语法

第一部分,我们先来复习一下C语言的基本语法,在这里,我不会覆盖到C语言的所有语法,而会列出一些C语言里容易引起困惑的知识点以及gcc编译器与其他编译器不同之处.完整的C语言语法,请参考C11国际标准以及GCC官方文档.

变量

bool类型变量

在gcc中,布尔类型的变量以_Bool形式表示,且只能赋值为01.

stdbool.h文件中,以宏定义的形式定义了bool,true,false等关键字.

#include<stdio.h>
#include<stdbool.h>	// 若注释掉此文件,会报错: unknown type name ‘bool’;‘true’,'false' undeclared 

int main() {

	bool b1 = true, b2 = false;
    
    return 0;
}

sizeof

sizeof是一个特殊的运算符,用来计算某变量或数据类型在内存中所占的字节数.

  • 若传入的变量为数据类型或单个变量,则返回该类型或该变量所占的字节数.
  • 若传入的变量为数组名,则返回该数组所占的字节数
#include<stdio.h>

int main() {

    int m = 10;
    int a[] = {1, 2, 3, 4, 5};
    int *p = a;
    
    printf("%d\n", sizeof(int));	// int类型变量占4个字节
    printf("%d\n", sizeof(m));		// 变量 m 占4个字节
    printf("%d\n", sizeof(10));		// 变量 10 占4个字节
    printf("%d\n", sizeof(a));		// 数组 a 占20个字节
    printf("%d\n", sizeof(p));		// 指针 p 占8个字节(与处理器位数有关)
    
    return 0;
}

变量的存储类型

声明变量时的完整语法是<存储类型> <变量类型> <变量名>,其中<存储类型>常常不写,默认为auto.

变量的存储类型有如下四种:

关键字存储类型
auto(默认)默认型
static静态型
register寄存器型
extern外部型
  1. auto:默认型

    auto是默认的变量存储类型,说明变量只能在程序某个范围内(函数体内或函数的复合语句内)使用,其初值默认为随机值.

    #include<stdio.h>
    
    int main() {
    	{
    		auto int a = 10;
    	}
    	printf("%d", a);	// 报错: ‘a’ undeclared
        
        return 0;
    }
    
  2. static:静态型

    static类型的变量既可以在函数体内声明,也可以在函数体外声明.有以下两个性质:

    • static类型的变量存储在静态存储区,而不是在堆栈中.
    • 在程序运行过程中,static类型变量不会被回收,一直保存着上一次调用存入的值.
    #include<stdio.h>
    
    int main() {
        for (int i=0; i<5; i++) {
            static int a = 5;
            a++;
            printf("%d ", a);
        }
    
        return 0;
    }
    

    运行上述程序,得到6 7 8 9 10

  3. register:寄存器型

    register关键字建议编译器将该变量放入寄存器中,以加快程序的运行速度.若申请不到寄存器,则该变量退化为auto型.

    #include<stdio.h>
    
    int main() {
    
        register int a = 10;
        printf("a=%d %p", a, &a);	// 报错: address of register variable ‘a’ requested
    
        return 0;
    }
    
  4. extern:外部型

    extern用于说明该变量应用了外部作用域内声明的变量.下面例子演示其使用:

    创建文件global_1.c如下:

    int global_a = 10;
    

    创建文件global_2.c如下:

    #include<stdio.h>
    
    extern int global_a;
    
    int main() {
        printf("global a = %d\n", global_a);
    
        return 0;
    }
    

    将两个文件一起编译:

    gcc global_1.c global_2.c
    

    程序输出global a = 10.

    static修饰的变量,其他文件无法引用.

输入输出

使用scanf()读取字符时,有可能读到缓冲区内残余的一些垃圾.

#include<stdio.h>

int main() {
    int x;
    char ch;

    scanf("%d", &x);
    scanf("%c", &ch);
    printf("x=%d, ch=%c\n", x, ch);
    
    return 0;
}

输入10↵c后,程序输出x=10, ch=↵.这是因为scanf把我们用作分隔符的回车↵赋给了ch.

有两种方式解决这种问题:

  1. 使用getchar()函数清除缓冲区内容:

    #include<stdio.h>
    
    int main() {
        int x;
        char ch;
    
        scanf("%d", &x);
        getchar();		// 清空缓冲区
        scanf("%c", &ch);
        printf("x=%d, ch=%c\n", x, ch);
        
        return 0;
    }
    
  2. 使用模式匹配字符%*c匹配缓冲区内容:

    #include<stdio.h>
    
    int main() {
        int x;
        char ch;
    
        scanf("%d", &x);
        scanf("%*c%c", &ch);	// 或scanf(" %c", &ch);
        printf("x=%d, ch=%c\n", x, ch);
        
        return 0;
    }
    

运算符的优先级

优先级运算符结合规律
1[],(),.,->,后缀++,后缀--从左向右
2前缀++,前缀--,sizeof,&,^,+,-,!从右向左
3强制类型转换从右向左
4^,/,%(算数乘除)从左向右
5+,-(算术加减)从左向右
6<<,>>(位移位)从左向右
7<,<=,>,>=从左向右
8==,!=从左向右
9&(位逻辑与)从左向右
10^(位逻辑异或)从左向右
11|(位逻辑或)从左向右
12&&从左向右
13||从左向右
14? :从右向左
15=,^=,/=,%=,+=,-=,<<=,>>=,&=,^=,|=从右向左
16,从左向右

指针

指针与数组

  1. 指针与一维数组

    一维数组的数组名即为数组的指针,指向数组的起始地址.设指针px指向数组x[],则下面四种写法具有完全相同的意义:

    x[i],*(px+i),px[i],*(x+i).

    #include<stdio.h>
    
    int main() {
    	int x[] = {3, 6, 9};
        int *px=x, n=sizeof(x) / sizeof(int);
        
        printf("%p %p %p\n", x, x+1, x+2);	// 输出 0x7ffc45767d8c 0x7ffc45767d90 0x7ffc45767d94
    
        for (int i=0; i<n; i++) {
            printf("%d %d %d %d\n", x[i], *(px+i), px[i], *(x+i)); // 输出 3 3 3 3↵6 6 6 6↵9 9 9 9
        }
        
        return 0;
    }
    
  2. 指针与二维数组

    二维数组名为数组的行指针,指向数组的起始地址.数组名加1表示移动一行元素.设行指针px指向数组x[][],则下面四种写法具有完全相同的意义:

    x[i][j],*(*(px+i)+j),px[i][j],*(*(x+i)+j).

    行指针不同于二重指针,行指针用于存储行地址,其定义方式为<存储类型> <数据类型> (*<行指针变量名>)[每行元素数]

    在这里插入图片描述

    #include<stdio.h>
    
    int main() {
    	int x[][2] = {{1, 6}, {9, 12}, {61, 12}};
        int (*px)[2];	// 定义一行指针px,每行2个元素 
        
        px = x;
        
        printf("%p %p\n", x, x+1);		// 输出 0x7ffc539cf720 0x7ffc539cf728
        printf("%p %p\n", px, px+1);	// 输出 0x7ffc539cf720 0x7ffc539cf728
        
    	printf("%d %d %d %d\n", x[1][1], *(*(px+1)+1), px[1][1], *(*(x+1)+1));	// 输出 12 12 12 12
        
        return 0;
    }
    

    行指针二重指针的一个区别在于: 执行++操作时,行指针移动一行数据所占字节,而二重指针只移动一个指针变量所占字节.

  3. 字符指针与字符串

    在C语言中,字符串常量被存放在静态存储区内.字符串指针存储的是字符串的首地址,而不是其中的内容.

    #include<stdio.h>
    #include<ctype.h>
    
    int main() {
        
        char *p1 = "hello world";
        char *p2 = "hello world";
        
        printf("&p1=%p, p1=%p, %s\n", &p1, p1, p1);
        printf("&p2=%p, p2=%p, %s\n", &p2, p2, p2);	// 两指针指向同一块内存空间
        
        return 0;
    }
    

    运行上述程序,得到:

    &p1=0x7ffd013dc848, p1=0x559d63a357c4, hello world
    &p2=0x7ffd013dc850, p2=0x559d63a357c4, hello world
    

    在C语言中,有三类变量会被存入静态存储区: 全局变量,static修饰的变量,字符串常量.

    不能通过字符串指针索引修改字符串,但可以通过数组名索引修改字符串数组.

    #include<stdio.h>
    #include<ctype.h>
    
    int main() {
        
        char array[] = "hello world";	// 字符串数组
        char *pointer = "hello world";	// 字符串常量
        
        // 字符串数组和字符串常量分别存储在内存的不同区域
        printf("&array=%p, array=%p, %s\n", &array, array, array);
        printf("&pointer=%p, pointer=%p, %s\n", &pointer, pointer, pointer);
        
        // 可以通过数组名索引修改该字符串数组的内容
        array[3] = 'o';		// 或 *(array+3)='o';
        printf("&array=%p, array=%p, %s\n", &array, array, array);
    
        // 不可以通过指针名索引修改字符串常量中的内容
    	pointer[3] = 'o';	// 或 *(pointer+3)='o'; 
        printf("&pointer=%p, pointer=%p, %s\n", &pointer, pointer, pointer);	// 发生Segmentation fault 错误
        
        return 0;
    }
    

    可以看到,尽管内容相同,arraypointer变量被保存在内存的不同地址.且当执行到pointer[3] = 'o';语句时,发生Segmentation fault错误.

    &array=0x7ffe8e5639ac	array=0x7ffe8e5639ac	hello world
    &pointer=0x7ffe8e5639a0	pointer=0x55ec90415814	hello world
    &array=0x7ffe8e5639ac	array=0x7ffe8e5639ac	heloo world
    Segmentation fault (core dumped)
    

常量指针和指针常量

  • 常量指针的声明方式为:<数据类型> * const <指针变量名称>.

    常量指针所指向的变量被视为一个常量,不可通过指针修改其值.但指针本身可以修改.

  • 指针常量的声明方式为:const <数据类型> * <指针变量名称>.

    指针常量本身不能修改.但我们可以通过* <指针名>修改所指向的变量的值.

const关键字修饰谁,谁就是常量.

#include<stdio.h>

int main() {
    
    int n = 10;
    
    const int * p1 = &n;	// 常量指针,不能通过p1修改n的值,但p1本身可以修改
    int * const p2 = &n;	// 指针常量,可以通过p2修改n的值,但p2本身不能修改
    
    // *p1 = 20;	// 报错: assignment of read-only location ‘*p1’
    p1 = NULL;		// 可以修改p1本身
    *p2 = 20;		// 可以通过p2修改其所指向的变量n
    // p2 = NULL;	// 报错: assignment of read-only variable ‘p2’

    return 0;
}

p1的定义语句中,const修饰的是int * p1整体,因此将所指向的量(location)视为常量;在p2的定义语句中,const修饰的是p2本身,因此将指针变量本身(variable)视为常量.

函数

main()函数参数的意义

在C语言中,main()函数完整的函数原型是int main(const int argc, const char * argv[]).

其中argc代表传入程序的参数个数+1;argv[0]指向函数名字符串,argv的其他元素依次指向传入程序的各个参数.

以下面程序为例,创建文件show_args.c,内容如下:

#include<stdio.h>

int main(const int argc, const char * argv[]) {
    printf("argc=%d\n", argc);

    for (int i=0; i<argc; i++) {
        printf("%s\n", argv[i]);
    }
    
    return 0;
}

使用gcc编译该程序:gcc show_args.c -o show_args,生成可执行文件show_args.

调用该可执行文件并传入参数:./show_args 192.168.1.7 6000,程序输出如下:

argc=3
./showargs
192.168.1.7
6000

数组作为函数参数

使用数组作为函数参数时,形参可以写成指针的形式(void fun(int* array)),也可以写成数组名的形式(void fun(int array[])).但不管哪种书写形式,形参都会被视为指针而非数组.

下面程序中的array_sum()函数试图对数组进行求和,但是算出了错误的结果,因为函数的形参array被视为指针而非数组,对其调用sizeof运算符得到的是数组指针所占字节数,而非数组所占字节数.

#include<stdio.h>

int array_sum(int array[]);	// 等价于 int array_sum(int * array); 本质上是一样的	

int main() {
    int a[] = {1, 2, 3, 4, 5};
    int sum = 0;

    printf("实参数组地址: %p", a);
    sum = array_sum(a);

    return 0;
}

int array_sum(int array[]) {
    printf("形参数组地址: %p", array);
    int sum = 0;
    int n = sizeof(array)/sizeof(int);	// 在这里,array被视为指针而非数组名,造成bug
    
    for (int i=0; i<n; i++) {
        sum += array[i];
    }
    
    return sum;
}

要在函数中对传入数组进行遍历,一定要向其传入数组的长度,上面程序可以这样改写:

int array_sum(int array[], int n) {
    printf("形参数组地址: %p", array);
    int sum = 0;
    // int n = sizeof(array)/sizeof(int);	// 在这里,array被视为指针而非数组名,造成bug
    
    for (int i=0; i<n; i++) {
        sum += array[i];
    }
    
    return sum;
}

函数指针

函数指针用于存放函数的入口地址,可以通过函数指针对调用函数,有点类似于其他高级语言中的匿名函数.声明函数指针的语法如下:

<数据类型> (*<函数指针名称>)(<参数列表>)

下面例子演示函数指针的使用:

#include<stdio.h>

int add(int a, int b) {
    return a+b;
}

int sub(int a, int b) {
    return a-b;
}

int multiply(int a, int b) {
    return a*b;
}

int main() {    
    int(*p)(int, int);	// 定义一个函数指针p,对应的参数列表为(int, int),返回值为int
    
    p = add;
    printf("%d\n", (*p)(30, 20));	// 输出 50
    
    p = sub;
    printf("%d\n", (*p)(30, 20));	// 输出 10
    
    p = multiply;
    printf("%d\n", (*p)(30, 20));	// 输出 600
}

函数指针广泛应用于各种库函数中,例如下面几个函数:

  • 排序函数qsort()需要一个函数指针参数指定数据的比较方法:

    void qsort(void *base, size_t nmemb, size_t size, 
    	int (*compar)(const void *, const void *));
    
  • 线程创建函数pthread_create()需要一个函数指针参数指定线程执行的任务:

    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
    	void *(*start_routine) (void *), void *arg);
    

条件编译

C语言的条件编译可以根据宏定义编译代码,这是在预处理阶段进行的.

条件编译指令说明
#ifdef如果该宏已定义,则执行相应操作
#ifndef如果该宏没有定义,则执行相应操作
#if如果条件为真,则执行相应操作
#else如果前面条件均为假,则执行相应操作
#elif如果前面条件为假,而该条件为真,则执行相应操作
#endif结束前面对应的条件编译指令

下面两个程序展示了条件编译的使用

#include<stdio.h> 
#define _DEBUG_		// 定义宏变量_DEBUG_

int main() {    
# ifdef _DEBUG_
    printf("The macro _DEBUG_ is defined.\n");
# else
    printf("The macro _DEBUG_ is not defined.\n");
# endif
    
    return 0;
}
#include<stdio.h>
#define _DEBUG_ 1	// 定义宏变量_DEBUG_并赋值为1

int main (void)
{
#if !_DEBUG_ 
    printf("The macro _DEBUG_ is 0.\n");
#else
    printf("The macro _DEBUG_ is 1.\n");
#endif
    
    return 0;
}

结构体和共用体

结构体

结构体变量的定义有3种方法

  • 先定义结构体类型再定义变量

    struct 结构体名 {
        成员列表;
    };
    
    struct 结构体名 变量名1, 变量名2;
    
  • 在定义结构体的同时定义变量

    struct 结构体名 {
        成员列表;
    } 变量名1, 变量名2;
    
  • 不定义结构体类型名,直接定义结构体变量

    struct {
        成员列表;
    } 变量名1, 变量名2;
    

    这种方式常用于定义结构体内部嵌套的结构体,类似于其他高级语言的匿名类.

    struct student {
        int id;
        char name[20];
        struct {
            int year;
            int month;
            int day;
        } birthday;
    };
    
    struct student stu1 = {01, "zhangsan", {1998, 1, 1}};
    struct student stu2 = {02, "lisi", {1999, 1, 1}};
    

需要注意的是,结构体变量的变量类型为struct 结构体名,单独使用结构体名声明变量时不被允许的,即使在VC等编译器中可以这样做.

struct student {
    int id;
    char name[20];
};

// student stu1;	// 报错: unknown type name ‘student’; use ‘struct’ keyword to refer to the type
struct student stu2;

可以使用typedef给结构体类型取别名.在下面的例子中,我们定义了两个新的数据类型listnodelinklist,它们分别等价于struct _node_struct _node_ *.

tyoedef struct _node_ {
    int data;
    struct _node_ *next;
} listnode, *linkist;

共用体

使用union关键字定义共用体,定义共用体变量的语法和结构体类似,他们的本质区别仅在于内存的使用方式上.共用体中的变量成员占据同一处内存空间,为共用体的一个成员变量赋值后,其它成员变量就失去了作用.

#include<stdio.h>

struct struct_example {
    int i;
    char c;
    double d;
};

union union_example {
    int i;
    char c;
    double d;
};

int main() {   
    printf("size of struct struct_example = %d\n", sizeof(struct struct_example));	// 输出16
    printf("size of union union_example = %d\n", sizeof(union union_example));	// 输出8
}

内存管理

C语言的内存区间

C/C++语言中定义了四个内存区间:

  • 代码区
  • 全局变量与静态变量区
  • 局部变量区即栈区:效率高,空间小.
  • 动态存储区即堆区:效率低,空间大.

局部变量存储在栈区,而动态内存分配发生在堆区.

动态内存分配

动态内存分配发生在堆区,在分配堆区数据时不会做初始化操作(即数据初值不是0),必须显示初始化.

使用malloc()free()函数进行堆区内存的申请和释放,两个函数的原型如下:

void* malloc(size_t size);
void free(void *ptr);

malloc(size_t size)函数本身并不关心所申请的内存是什么类型,只关心申请内存的总字节数size,返回一个void *指针,需要我们将其显式转换成我们所需要的类型.

malloc()函数申请到的是一片连续的内存空间,有时会申请不到符合大小的内存,返回NULL,因此我们需要对malloc()函数的返回值进行判断后再进行下一步操作.

free(void *ptr)函数的作用是删除指针ptr指向的变量,回收其内存空间,而不是删除ptr指针本身,调用free()函数过后,ptr变为了野指针,为了安全着想,应尽快将其设为NULL.

内存泄漏: 若malloc()函数返回的指针值丢失,我们就无法访问该内存区域,进而无法回收该内存区域.这被称为内存泄漏现象.为避免内存泄漏现象的产生,我们应尽量保证malloc()free()成对出现在同一函数体内.

使用动态内存分配可以使得变量的定义更加灵活.下面的例子中,需要将get_string()函数中的字符串变量s传回主函数,使用其它方式定义变量都会存在各种问题,只能使用动态内存分配的方式.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

char * get_string() {
    
    // char s[] = "welcome";		// 数组s是局部变量,无法将内容传递到函数外
    // char * s = "welcome";		// 指针s指向字符串常量,无法修改其内容 
    // static char s[] = "welcome";	// 数组s被存储在静态存储区,程序结束之前无法回收该块内存
    char * s = (char *)malloc(10 * sizeof(char));
    if (s == NULL) {	// 对malloc()函数的返回值应先判断再使用
        printf("malloc failed\n");
        return NULL;
    }
	strcpy(s, "welcome");
    
    return s;
}

int main() {
    char * p;
    p = get_string();
    printf("%s\n", p);
    
    free(p);
    p = NULL;	// 释放内存后应尽快将对应指针设为NULL;
    
    return 0;
}
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值