C语言摘要 -- K&R笔记(2)

本文总结了《K&R》第4章关于C语言函数的要点,包括函数定义、声明与调用的规则,以及外部/全局变量和内部/局部变量的区别。讨论了变量作用域、extern关键字、变量名冲突解决以及头文件的使用。还简述了预处理阶段的宏定义和条件包含等概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最新文章请访问:SHIZHZ's Blogsicon-default.png?t=LBL2https://shizhz.me/

本文是对《K&R》第4章《Functions and Program Structure》的一个摘要。

1. 函数定义范式为:

return-type fun-name(arguments declarations)

{

    declarations and statements

}

在函数定义中如果没有指定返回值,则默认为int型,通过-Wall编译器会显示相应的警告信息:

fun() {}

单独编译时:

$ gcc -c fun.c -Wall
fun.c:1:1: 警告:返回类型默认为‘int’ [-Wreturn-type]
fun.c: 在函数‘fun’中:
fun.c:1:1: 警告:在有返回值的函数中,控制流程到达函数尾 [-Wreturn-type]

-c选项告诉gcc不调用连接器(linker),只生成可重定位的目标文件,该文件格式为ELF,内容可通过readelf或objdump等二进制工具读取,更多二进制工具可参考GNU提供的binutils;-Wall选项告诉gcc显示所有警告信息。

2. 函数的声明、定义及调用:

函数的声明与定义需要一致,编译器通过检查函数名、返回值、参数列表来检查函数声明与定义的一致性,对于隐式声明的函数则不检查参数列表。函数调用前进行显示声明是好的编程习惯。

如果没有显示声明,则调用处包含了对函数的一个隐式声明,隐式声明的返回值为int,参数任意。如果该函数定义在同一个源文件中,则编译器会对返回值进行一致性检查:

#include <stdio.h>
int main (int argc, char const* argv[])
{
    printf("result: %d\n", fun(5, 10));
    return 0;
}

char* fun(int i)
{
    printf("%d\n", i);
    return "hello, world\n";
}

函数fun是在调用处隐式声明的,返回值为int,而定义时返回值为char *;编译时出错:

$ gcc -Wall main.c 
main.c: 在函数‘main’中:
main.c:4:5: 警告:隐式声明函数‘fun’ [-Wimplicit-function-declaration]
main.c: 在文件作用域:
main.c:8:7: 错误:与‘fun’类型冲突
main.c:4:28: 附注:‘fun’的上一个隐式声明在此

如果隐式声明的函数定义在另一个源文件中,则编译器无法进行类型检查,因为编译器对各源文件是分别进行编译最后再链接的,如:

// file main.c
#include <stdio.h>
int main (int argc, char const* argv[])
{
    printf("result: %d\n", fun(5, 10));
    return 0;
}
// file fun.c
#include <stdio.h>

char* fun(int i)
{
    printf("in fun: %d\n", i);
    return "hello, world\n";
}

则编译时编译器只提示了存在函数隐式声明的警告信息,不会出错:

$ gcc -Wall main.c fun.c
main.c: 在函数‘main’中:
main.c:4:5: 警告:隐式声明函数‘fun’ [-Wimplicit-function-declaration]

但执行结果不可预料:

$ ./a.out 
in fun: 5
result: 4195934

此处也可以证明编译器没有对参数列表进行检查,调用时传递了两个参数,而fun的定义只接收一个参数,实际结果是只有第一个int参数有效。

3. 外部变量(external variables)/全局变量(global variables)vs 内部变量(internal variables)/局部变量(local variables):
定义在任何函数外部的变量是外部/全局变量,函数参数及定义在函数内部的变量为改函数的内部/局部变量;前者可以被所有函数访问,后者只能被对应的函数访问。C中函数名是外部/全局变量;对于需要多个函数共享的数据,可以定义为外部/全局变量,需要同一个函数在多次调用时共享的数据,可以定义为static修饰的内部/局部变量。

变量初始化:局部变量存在与函数的运行栈中,不会被初始化,但被static修饰的局部变量不在栈中保存;全局变量根据是否已经初始化存在于不同的块中,已初始化的全局变量存在于目标文件的.data块,未初始化的全局变量存在于.bss块,其值初始化为0。

4. 变量作用域:

a. 全局变量在整个程序中可用;

b. static修饰的全局变量在其定义的单个文件范围内可用;

c. static修饰的局部变量在其定义的函数范围内可用,可在多次调用时共享数据:

#include <stdio.h>
int main (int argc, char const* argv[])
{
    int fun(void); // 对fun函数的声明
    fun();
    fun();
    fun();
    fun();
    return 0;
}

int fun(void)
{
    static int counter; // counter会被初始化为0
    printf("fun called %d times\n", ++counter);
}

运行结果:

$ ./a.out 
fun called 1 times
fun called 2 times
fun called 3 times
fun called 4 times

d. 局部变量在其定义的{}块中可用;

#include <stdio.h>
int main (int argc, char const* argv[])
{
    int i = 5;
    {    // just a simple inner {} block
        int i = 6;
        printf("inner block: %d\n", i);
    }
    printf("outer block: %d\n", i);
    return 0;
}

结果为:

$ ./a.out 
inner block: 6
outer block: 5

可见如果在for,while或if等语句的{}块中所声明的变量,其作用域仅仅局限于该语句块中。

5. extern关键字:修饰变量声明,如果在源文件a.c中需要访问定义在其他源文件中的全部变量,则需要在a.c文件中对改变量进行声明,用extern关键字进行修饰意在告诉编译器改变量定义在别处,但在本文件中按照所声明的类型进行使用。好处有:1). 防止变量名冲突; 2). 编译器不会为被extern所修饰的全局变量分配内存空间,从而节约资源;

6. 变量名冲突:

a. 局部变量:在同级作用域内,不允许存在相同的变量名声明;

b. 全局变量:

首先需要理解的是编译器的工作流程为:预处理 --> 编译 --> 汇编 --> 链接。截止到汇编,编译器都是对单个源文件进行处理,生成可链接的目标文件(object file),每个目标文件都有对应的符号表记录了对全局变量的声明/定义/调用等信息;然后由链接器完成对所有全局变量的符号解析与重定位,生成最后的可执行文件。

链接器在处理全局变量的符号时,将其分为强(strong)符号与弱(weak)符号。函数名与已初始化的全局变量为强符号,未初始化的全部变量为弱符号,对于多处定义的符号,链接规则为:

i. 不允许有多个强符号;

// main.c
int var = 105;
int main (int argc, char const* argv[])
{
    return 0;
}
// fun.c
int var = 105;

链接时出错:

$ gcc -Wall main.c fun.c 
/tmp/cchyDXqW.o:(.data+0x0): multiple definition of `var'
/tmp/cc8RjM41.o:(.data+0x0):第一次在此定义
collect2: 错误:ld 返回 1

ii. 一个强符号与多个弱符号:选择强符号;

// main.c
#include <stdio.h>

int var = 105;
int var;
int fun();
int main (int argc, char const* argv[])
{
    printf("result of fun: %d;\n", fun()); // 不将这两行写成  printf("result of fun: %d; var: %d\n", fun(), var);  是因为函数参数的解析顺序可不预料. 参见上一篇《kr学习笔记1》
    printf("var: %d\n", var);
    return 0;
}
// fun.c
int var;

int fun()
{
    return var ++;
}

结果为:

$ ./a.out 
result of fun: 105;
var: 106

iii. 多个弱符号:任意选择一个;
编译器通常不会报告发现多个同名弱符号的定义。

7. 头文件(.h 文件):

将整个工程需要多处使用的全局变量集中起来声明是非常好的选择,这样就可以在需要使用的源文件中直接引入即可,于是就诞生了头文件。

头文件的引入由预处理指令include完成,在C中,#符号代表这是一条预处理指令;自定义的头文件引入格式:

#include "user.h"

标准库中的头文件引入格式:

#include <stdlib.h>

8. 预处理器:

如前所述,预处理是编译的第一阶段,预处理指令由#符号标识。

#include 引入文件,预处理时读入对应文件的内容;

#define 宏定义,预处理时替换源文件中所有的宏;

#undef 取消宏定义;

条件包含:

#if !defined(EG)
#define EG
#endif

#ifdef或#ifndef

// do something

#elif

// do something else

#else

// do something default

#endif
可传递预处理参数给gcc,查看对应预处理结果。

 ________________ 
< 本篇终了:) >
 ---------------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||-----w |
                ||       ||

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值