C语言 其他
作用域
关键字
register
在C语言中,register
关键字是一种存储类说明符,用于建议编译器将变量存储在CPU寄存器中,而不是内存中。这样做可以提高访问变量的速度,因为寄存器的访问速度通常比内存快得多。然而,这个关键字只是一个提示,编译器可以选择忽略它。
使用register关键字的变量通常用于循环或频繁访问的代码段中,以提高程序的性能。例如:
register int count = 0;
使用 register
关键字声明的变量不能与取地址运算符 &
一起使用。这是因为 register
变量通常被存储在CPU的寄存器中,而不是内存中,因此它们的地址是不可获取的。
例如,以下代码是不允许的:
register int x = 10;
int *ptr = &x; // 错误:不能获取寄存器变量的地址
编译器会报错,因为x是一个寄存器变量,你不能获取它的地址。
总结:
register
只是建议,编译器是可以- 只要使用了
register
修饰变量,就不能通过&
来获取变量的地址值了。
volatile
音标:ˈvɒlətaɪl
, 意思:易变的
volatile
关键字是一种类型限定符,用于告诉编译器该变量可能会在程序的控制之外被改变。这通常用于多线程环境中,或者当变量与硬件设备(如寄存器或内存映射的输入/输出端口)相关联时。
使用 volatile
关键字的变量,编译器将不会对该变量进行优化,因为它认为每次读取或写入这个变量都可能得到不同的值。这确保了程序在每次访问 volatile
变量时都会从内存或硬件设备中获取最新的值,而不是使用寄存器中的值或进行其他优化。(实时更新, 不会因为多次赋值的操作就优化赋值,直接将最后一次赋值做赋值操作其他不了。)
int a = 10;
a = 11;
a = 17; // 这段代码会被编译器优化 只对内存进行 a = 17 赋值操作
volatile int b = 10;
b = 11;
b = 17; // 这段代码不会被优化,每次赋值都会真实进行一次读写
extern
extern
关键字用于声明一个变量或函数,表明它们在其他文件中定义,并且可以在当前文件中使用。这种声明告诉编译器,这些标识符的存储位置和初始化是在其他地方完成的。
用途:
- 跨文件共享变量:当你需要在多个文件中使用同一个变量时,可以在一个文件中定义变量,并在其他文件中使用
extern
来声明它。 - 跨文件共享函数:如果你想在多个文件中调用同一个函数,可以在一个文件中定义函数,并在其他文件中使用
extern
来声明它。
例子:
int id = 10086;
int add(int a,int b) {
return a + b;
}
// a.c
#include <stdio.h>
extern int id;
extern int add(int a, int b);
int main(int argc, char const *argv[])
{
printf("id = %d \n", id);
printf("%d \n", add(1, 2));
return 0;
}
运行:
gcc a.c b.c -o demo
./demo
局部作用域
#include <stdio.h>
int main(int argc, char const *argv[])
{
int a=1;
int b=2;
printf("a= %d \n", a); // 1
{ // 大括号可以在代码中开启一段新的作用域
printf("a= %d \n", a); // 1
int c=4;
int d=5;
int a = 100;
printf("c= %d \n", c); // 4
printf("a= %d \n", a); // 100
}
// printf("c= %d \n", c); // 不能访问
// 变量的访问遵循就近原则,任何一个变量都是从当前作用域内他前面的代码查找是否有该变量
// 如果没有就去上一级作用域查找,直到全局作用域.若全局作用域也没有那就报错
return 0;
}
- 代码块指的是一对花括号 { } 括起来的区域。
- 代码块可以嵌套包含,外层的标识符会被内嵌的同名标识符临时掩盖变得不可见。
- 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。
全局作用域
- 概念:在代码块外定义的变量,其可见范围可以跨越多个文件。
- 示例:
// 文件:a.c
int global = 888; // 变量 global 的作用域是第2行到本文件结束
int main()
{
}
void f()
{
}
// 文件:b.c
extern int global; // 声明在 a.c 中定义的全局变量,使其在 b.c 中也可见
extern void f();
void f1()
{
printf("global: %d\n", global);
}
void f2()
{
f();
}
- 全局变量作用域在整个程序中,因此称其为全局变量
- 如果a文件想要访问b文件的全局变量,需要在a文件函数体外,使用extern关键字在变量前面 进行修饰
- 如果一个文件里的全局变量,只想作用于本文件,则在其前面加上static即可
编译过程
编译过程分为以下四个阶段:
- 预处理:处理预处理语句(#开头的语句),删除注释、头文件展开、宏替换...
gcc hello.c -o hello.i -E - 编译:将C语言程序转化为汇编语言
gcc hello.c -o hello.s -S - 汇编:将程序代码转化为二进制代码
gcc hello.c -o hello.o -c - 链接:将所有二进制代码合并起来,根据应用规则生成一个专门针对某个平台执行的应用程序镜像
gcc hello.c -o hello
宏定义
宏的概念
宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:
#define PI 3.14
在程序编译时,预处理阶段将宏名称替换成其定义的内容
语法:
// 不带参数的
#define 宏名称 字符串
// 带参数
#define 宏名称(参数列表) 字符串
例子:
#include <stdio.h>
#define PI 3.14
#define MAX(a, b) a > b ? a : b
int main(int argc, char const *argv[])
{
printf("%f \n", PI);
printf("%d \n", MAX(7, 8));
return 0;
}
注意事项
观察以下代码, 我们定义一个宏 N 为 2 + 1
, int a = N*N
我们想让他为3*3的效果实际上是 2+1*2+1
效果
#include <stdio.h>
#define N 2 + 1
int main(int argc, char const *argv[])
{
int a = N * N;
printf("%d \n", a); // 5
}
由此开发者注意,宏定义在开发中非常有用可以提高代码的可读性、可维护性,但是宏定义本身只是一个简单地文本替换
- 没有类型检查
printf("%d \n", MAX("hello", 8)); // 不报错
- 因为运算符的优先级导致宏字符串可能不能直接表达一个整体
int a = N * N; // int a = 1+2*1+2
所以在使用宏定义声明表达式字符串的时候加小括号保证宏定义的含义可以正确的传达, 在使用宏时一定要正确传递参数
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define N (2 + 1)
int main(int argc, char const *argv[])
{
int a = N * N;
printf("%d \n", a); // 5
printf("%d \n", MAX(7, 8));
return 0;
}
复杂宏定义
在c语言中宏定义可以是非常复杂的,宏定义可以结合参数、操作符、条件、嵌套创建功能强大的宏。
一般情况下,复杂的宏定义会喜欢使用do...while语句
#define IS_EVEN(n) (n) % 2 == 0
#define PRINTF_NUMBER(n) do { \
if(IS_EVEN(n)) printf("%d 是偶数 \n" , n); \
else printf("%d 是奇数 \n" , n ); \
} while(n<=0)
// n-- // 错误的
int main() {
PRINTF_NUMBER(-9); // 宏传递的参数是不能修改,原因是宏不进行数据类型检查,直接进行文本替换,
return 0;
}
注意点:
- 宏默认只支持一行书写,如果需要写多行需要再行的最后面加上
\
表示换行(但是在进行预处理的时候也是替换成一行的) - 在宏定义的宏行参是不能修改的。宏是不进行数据类型检查的,比如参数传递的一个字符串 “hello” , 宏的代码是 N++ , 替换就成了 "hello"++ ; 这个语法是错误的。
- 在宏定义中可以定义变量的,也可以将 形参赋值个定义的变量
- 注意在宏中定义变量主要不要函数中变量同名了,原因是:宏本质是文本替换,如果同名就有可能出现重复定义的问题
预定宏
在C语言中标准编译器预先定义了一些宏,这些宏无需定义直接可以使用
printf("%s \n",__DATE__); // 当前日期
printf("%s \n",__TIME__); // 当前时间
printf("%s \n",__TIMESTAMP__); // 当前时间日期
printf("%s \n",__FILE__); // 当前文件名
printf("%d \n", __LINE__); // 当前行
printf("%s \n", __FUNCTION__); // 当前函数名
符号
将传入的内容转字符串
#include <stdio.h>
// 宏名字不重要, #x 才是表示将参数转换为字符
#define STRI(x) #x
int main(int argc, char const *argv[])
{
char ch[200] = STRI(hello);
printf("%s \n" , ch);
return 0;
}
符号
#include <stdio.h>
// 宏名字不重要,是什么都没有关系主要是 x ## y 代表将两个参数组成一个变量名
#define PASTE(x, y) x ## y
int main() {
int GetAge = 17;
printf("%d \n", PASTE(Get,Age)); // 正确展开为 printf("%d \n", GetAge);
return 0;
}
补充: 连续字符串有自动连接功能,字符串字面量是常量,通常存储在只读数据段中。当你将多个字符串字面量写在一起时,它们会被编译器自动连接起来。
char *p = "hello" "world";
printf("%s \n", p);
char a[] = "hello" "world";
printf("%d \n", sizeof(a));
内联函数与宏函数的的区别
C语言中的内联函数和宏函数都是用于优化代码性能的工具,但它们之间存在一些重要的区别。
编译时处理
- 宏函数:宏函数在预处理阶段由预处理器处理。预处理器会直接将宏函数替换为它的定义,然后再进行编译。因此,宏函数没有类型检查,也没有作用域,只是简单的文本替换。
- 内联函数:内联函数在编译时被处理。编译器会在调用内联函数的地方直接插入或替换内联函数的代码,从而避免函数调用的开销。内联函数有类型检查,也有作用域。
参数处理
- 宏函数:由于宏函数只是文本替换,因此参数在替换过程中可能会引发一些意料之外的问题,比如运算符优先级问题。为了避免这些问题,宏函数的参数通常需要用括号包围。
- 内联函数:内联函数在参数处理上就像普通的函数一样,有明确的参数类型和参数传递方式,因此不需要担心运算符优先级等问题。
调试和可读性
- 宏函数:由于宏函数是文本替换,因此在调试时可能会比较困难,因为预处理器已经将宏函数替换为了它的定义,所以无法在调试器中看到宏函数的调用。此外,由于宏函数没有明确的作用域和类型检查,因此可能会降低代码的可读性和可维护性。
- 内联函数:内联函数像普通的函数一样,可以在调试器中看到其调用和执行过程,因此更易于调试。此外,内联函数有明确的作用域和类型检查,因此可以提高代码的可读性和可维护性。
适用场景
- 宏函数:通常用于执行简单的、不需要复杂逻辑或类型检查的操作,或者用于定义一些常量或简单的计算式。
- 内联函数:适用于那些函数体较小,但又频繁调用的函数。通过内联可以减少函数调用的开销,提高程序的执行效率。
总的来说,内联函数和宏函数各有其优缺点,应根据具体的应用场景和需求来选择使用哪一种。
#define MAX(a,b) ((a)>(b)?(a):(b))
MAX(a,"Hello"); //错误地比较int和字符串,没有参数类型检查
#include <stdio.h>
inline int add(int a, int b)
{
return (a + b);
}
int main(void)
{
int a;
a = add(1, 2);
printf("a+b=%d\n", a);
return 0;
}
以上a = add(1, 2);处在编译时将被展开为:
a = (a + b);
条件编译
我们在开发中经常使用 if...else 语句实现代码分支结构, c语言同样支持在预处理中进行分支结构
例子
#include <stdio.h>
#define N 1
int main() {
int c = 1;
#if N == 0 // 只有满足条件才会出现在预处理后文件中,只有过了预处理才会进行编译
c = 2;
#endif
printf("%d \n", c); // 输出 1
return 0;
}
条件编译支持多分支语句
#define A 0
#define B 1
#define C 2
#if A
... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif
// 二路分支
#if A
...
#elif B
...
#endif
// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif
条件编译支持 #else
// 二路分支
#if MACRO == 0
...
#else
...
#endif
注意:
- 可以实现条件编译的原因是在预处理的时候有些代码就已经不存在了。
#if
必须要和#endif
一起使用
#ifdef #ifndef 指令
解释: #ifdef
表示是否定义了某个宏
#ifndef
表示是否没有该宏
#ifdef ABC
printf("执行这段代码 \n");
#else
printf("else 可以跟 ifdef 和 ifndef 一起使用\n");
#endif
#ifndef ABC
printf("执行另一端这段代码 \n");
#endif
条件编译应用场景
- 平台依赖性代码:
不同操作系统或硬件平台可能需要不同的代码实现。通过条件编译,可以为不同的平台编写特定的代码。
#ifdef _WIN32
// Windows平台的代码
#else
#ifdef __linux__
// Linux平台的代码
#endif
_WIN32
和__linux__
是对应操作平台预定义的宏,用于检测编译时的操作系统
- 调试和发布版本:
在调试版本中,可能需要包含额外的调试信息或断言检查。在发布版本中,这些代码可以被排除以提高性能。
// demo.c
#ifdef DEBUG
#include <assert.h>
#define ASSERT(x) assert(x)
#else
#define ASSERT(x)
#endif
输入指令 gcc demo.c -o demo -DDEBUG
- 功能开关:
可以通过条件编译来启用或禁用某些功能,而不需要修改源代码。
#ifdef ENABLE_FEATURE_X
// 启用功能X的代码
#endif
- 第三方库的兼容性:
如果不同的库提供了相同的功能,可以使用条件编译来选择使用哪个库。
#ifdef USE_LIB_A
#include "lib_a.h"
#else
#include "lib_b.h"
#endif
- 编译时配置选项:
允许用户在编译时通过定义宏来配置程序的行为。
#define MAX_CONNECTIONS 100
- 代码重用:
通过条件编译,可以将通用代码和特定代码结合在一起,减少代码重复。
#ifdef USE_SSL
#include "ssl.h"
#endif
避免编译错误:
在某些情况下,某些代码可能不兼容某些编译器或平台。通过条件编译,可以避免这些代码被编译。
#ifdef __GNUC__
// GCC特有的代码
#endif
- 测试和维护:
在开发过程中,可能需要临时排除某些代码段以测试其他部分。条件编译提供了一种快速切换的方法。
#ifdef TEST_MODE
// 测试模式下的代码
#endif
- 多语言支持:
对于需要支持多种语言的应用程序,可以使用条件编译来选择不同的语言资源文件。
#ifdef LANGUAGE_EN
#include "en_resources.h"
#else
#include "de_resources.h"
#endif
条件编译和头文件
-
main.c
#include "common.h" int main() { sayHello(); printf("%d \n", globalAge); User *user = malloc(sizeof(User)); strcpy(user->password, "12345"); strcpy(user->username, "zhangsan"); user->id = 19; printf("%s %s %zd \n", user->username, user->password, user->id); free(user); return 0; }
-
tool.c
#include "common.h" int globalAge = 18; void sayHello() { printf("hello %d \n", globalAge); printf("%zu \n", sizeof(User)); }
-
tool.h
#ifndef __TOOL_H__ #define __TOOL_H__ #include "common.h" // 头文件主要是声明 函数、变量,不初始化 extern int globalAge; void sayHello(); typedef struct user { char username[20]; char password[20]; unsigned long id; } User; #endif
-
common.h
// 解释:如果没有定义 COMMON_H 就定义宏,然后导入的一些头文件 // 理解:打了个标记,如果宏已经定义过了,就代表一下的头文件已经导入的过了,无需要再次导入的了 // 以后的头文件都需要怎么,防止重复导入和循环导入的问题 #ifndef COMMON_H #define COMMON_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include "tool.h" #endif
出现问题:
- 头文件相互导入的导致tool.h ,出现循环导入的头文件的问题
解决方法:
- 利用条件编译,中
#ifndef
和define
解决
理解:
- 就是利用的打标记,如果一个宏被定义了,就不执行导入头文件的了
类似 Java 中单利模式。对象为 null
才会创建对象。
头文件
头文件的作用
通常,一个常规的C语言程序会包含多个源码文件(.c),当某些公共资源需要在各个源码文件中使用时,为了避免多次编写相同的代码,一般的做法是将这些大家都需要用到的公共资源放入头文件(.h)当中,然后在各个源码文件中直接包含即可。
引用头文件
#include "head.h"
包含自定义的头文件,""
编译器会优先从当前文件夹查找同名头文件#include <stdio.h>
包含系统预定义的文件<>
编译器会优先从系统环境变量中找
编译多个文件的指令
gcc ./*.c -o demo
# *.c 表示指定目录下所有.c的文件
指定头文件目录
gcc ./src/*.c -o demo -I ./inc
头文件的内容
头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括:
- 全局变量的声明。
- 普通函数的声明。
- 静态函数的定义。
- 宏定义。
- 结构体、联合体、枚举的定义。
- 枚举常量列表的定义。
- 其他头文件。
注意: 在头文件中定义全局变量给多个c文件使用,会造成全局变量重复声明从而报错.
解决方法:
// global.h
extern int global_id;
// global.c
int global_id = 10086;
#include "global.h"
int main() {
global_id = 1234;
}
头文件的格式
由于头文件包含指令 #include 的本质是复制粘贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现一种情况是:头文件被重复包含。
- 使用条件编译,解决头文件重复包含的问题,格式如下:
#ifndef _HEADNAME_H
#define _HEADNAME_H
...
... (头文件正文)
...
#endif
其中,HEADNAME一般取头文件名称的大写
#ifndef _OTHER_H
#define _OTHER_H
#include "head.h"
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#endif
通过宏定义的中条件编译关键字来,给