Clang — C 语言基础

出于看一些源码的需要,比如 Redis,发现很多之前的 C 语法细节已经忘了,现在再补回来,时过境迁,现在的感觉就是 C 语言已经和当初上学看的那个东西完全不一样了,先上 hello world 镇楼。

#include <stdio.h>

int main()
{
    printf("hello world");
}

其中:

  • main 函数是一个特殊的函数名,每个程序都从 main 函数的起点开始执行。
  • #include <stdio.h> 用于告诉编译器在本程序中包含标准输入/输出库的信息。

变量和常量 #

声明 #

int lower, upper;
char c, line[1000];

所有变量必须先声明再使用。

定义 #

还可以在声明的同时进行初始化。

int main()
{
    int age = 18;
    printf("%d", age);
}

定义(define)与声明(declaration)是两种含义的词,“定义”表示创建变量或分配存储单元,而“声明”指的是说明变量的性质,但不分配存储单元,故这两个词应谨慎使用。

作用域 #

根据作用域可以分为局部变量全局变量

函数中的每个局部变量自在函数被调用时存在,在函数执行完毕退出时消失,故局部变量也叫自动变量

全局变量也叫外部变量,必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。使用时可以通过 extern 关键字显式声明,也可以通过上下文隐式声明。

int max;
int main()
{
    extern int age = 18;
    printf("%d", age);
}

变量名 #

变量名是由字母和数字组成的序列,且第一个字符必须是字母,下划线 _ 也被看作是字母,通常用于命名较长的变量名,但是由于库例程的名字通常以下划线开头,因此变量名不要以 _ 开头。

字母大小写是有区别的,通常变量用小写,常量用大写。还有一些 ifelseintfloat 等关键字是保留给语言本身使用的,不能当作变量名。

常量 #

可以使用 #define 声明常量,表明其值不能修改,类型为 const。

限定符 #

short 和 long #

通常 short 至少为 16位,而 long 至少为 32 位,而且 short 不得比 int 长,int 不得比 long 长。

signed 和 unsigned #

可用于限定 char 类型或任何整型。

const #

任何变量的声明都可以使用 const 限定符限定,该限定符指定变量的值不能被修改。

数据类型 #

int #

对于 int 类型,通常为 16 位,即取值范围在 -32768~+32767 之间,也有用 32 位表示的 int 类型。

所有整型都包括 signedunsigned 两种形式,即有符号和无符号形式,且可以表示无符号常量与十六进制字符常量。

char #

字符型,占用一个字节,可以存放本地字符集中的一个字符。

float #

float 通常为 32 位,属于单精度浮点型,更高精度的还有 long、double。

double #

双精度浮点型。

数组 #

定义数组时需要同时指定长度。

int ndigit[10];
char nchar[20];

类型转换 #

C 语言没有指定 char 类型的变量是无符号变量(signed)还是带符号变量(unsigned),为了保证程序的可移植性,如果要在 char 类型的变量中存储非字符数据,最好指定 signed 或 unsigned 限定符。

C 语言中很多情况下会有隐式的算术类型转换。

运算符 #

各种运算符之间存在优先级关系,下面从高到低介绍。

算术运算 #

优先级最高的是算术运算符:➕、➖、✖️、➗、%(取模)。

关系运算 #

>>=<<= 它们具有相同的优先级,仅次于它们的是:==!=

逻辑运算 #

最后是 与 &&、或 ||、非 ! 运算符。

自增和自减运算 #

++-- ,其中表达式 ++n 先将 n 的值递增1,然后再使用变量 n 的值,而表达式 n++ 则是先使用变量 n 的值,然后再将 n 的值递增1。

位运算符 #

& 按位与(AND)

| 按位或(OR)

^ 异或(XOR)

<< 左移

>> 右移

~ 按位求反

赋值运算符 #

比如 =+=-=*=/= 都是,还有 %<<>>&^| 都可以与 = 相连。

条件表达式 #

比如 ><,也比如 (n>0)?f:n 也是。

sizeof 运算符 #

控制流 #

while #

do … while #

for #

break 和 continue #

以上三种循环都可以用这两个关键字。

if … else if … else #

switch #

goto #

goto 一般被认为更难以理解和维护,所以不鼓励使用。

函数 #

传参 #

形参和实参的概念。

返回值类型 #

如果函数定义中省略了返回值类型,则默认为 int 类型。

作用域规则 #

外部变量或函数的作用域从声明它的地方开始,到其所在的(待编译的)文件的末尾结束,即上面的函数无法直接引用其下面的函数,这就很不方便。

所以如果一定要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字 extern。比如:

#include <stdio.h>

int age;
void setage();

int main(){
    extern int age;
    printf("%d\n", age);
    setage();
    printf("%d\n", age);
}

void setage() {
    age = 18;
}
// 0
// 18

extern 不需要一定指定数组的长度。

静态变量(static) #

通常情况下,函数和外部变量是可以全局访问的,对于整个程序的各个文件都可见,但是,如果把函数或变量声明为 static 类型,则该函数/变量除了对该函数声明所在的文件可见外,其他文件都无法访问。

另外 static 也可以用于声明内部变量,在函数内声明后,该变量就是一种只能在某个特定函数中使用但一直占据存储空间的变量。

寄存器变量(register) #

通过 register 声明的变量是在告诉浏览器该变量在程序中使用的频率较高,其思想是,将 register 变量放在机器的寄存器中,这样可以使程序更小,执行速度更快,但编译器也可以忽略过量的或不支持的寄存器变量声明。

寄存器变量只适用于自动变量以及函数的形参。

初始化 #

在不显式初始化的情况下,外部变量和静态变量都将被初始化为 0,而自动变量和寄存器变量的初值则没有意义(即为无用的信息)。

字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的初始化表达式序列。例如:

char pattern[] = "ould";

等价于:

char pattern[] = {'o', 'u', 'l', 'd', '\0'};

此时数组的长度为 5。

递归 #

预处理器 #

头文件导入(#include) #

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。

#include 既可以用来引入库文件比如:

#include <stdio.h>

也可以用来引入当前位置其他源文件的头文件:

#include 'other.h'

对于中等规模的项目,或许一个头文件就可以解决,对于大型项目,可以考虑使用更多的头文件。

宏替换(#define) #

#define 是一种最简单的宏替换,使用 #define 来定义符号常量,代替一些固定的变量。

#include <stdio.h>
#define PAI 3.14

int main()
{
    printf("%f", PAI);
}

它还可以定义了一个语句:

#define forever for(;;) //无限循环

也可以定义一个带参数的表达式:

#define max(A, B) ((A)>(B)?(A):(B))

那么 x=max(p+q,r+s) 会被替换为:

x = ((p+q)>(r+s)?(p+q):(r+s));

还可以通过 #undef 指定取消名字的宏定义,这样做可以保证后续是函数调用,而不是宏调用。

#undef forever
#undef getchar

条件包含(#if) #

还可以使用条件语句对预处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算,为在编译过程中根据计算所得的条件值选择性地包含不同代码提供了一种选择。

下面这段预处理代码首先测试系统变量 STSTEM,然后根据变量的值确定包含哪个版本的头文件。

#if SYSTEM == SYSV
	#define HDR "sysv.h"
#elif SYSTEM == BSD
	#define HDR "bsd.h"
#else
	#define HDR "msdoc.h"
#endif

#include HDR

C 语言还专门提供了两个预处理语句 #ifdefifndef 用来测试某个名字是否已经被定义。

main 函数 #

对于 main 函数,可以有两个参数,一个是 argc,用于参数计数,表示运行程序时命令行中参数的数目,第二个参数是 argv 是一个字符串数组的指针,其中每个字符串对应一个参数。

main(int argc, char *argv[]){
  // 省略
}

数量可变的参数(...#

我们知道 printf 函数使用时可以随意填充参数数量。

char s;
int a, b;

printf("%s", s);
printf("%d, %d", a, b);

是因为 prinft 函数的声明为:

int printf(const char *fmt, ...);

printf 的内部,通过 va_list 声明这个可变参数变量,该变量将通过 va_start()va_arg()va_end 来完成对参数的依次引用。

void print_args(int count, ...) {
	int i, value;
	va_list arg_ptr;
	va_start(arg_ptr, count);
	for(i=0; i<count; i++) {
		value = va_arg(arg_ptr,int);
		printf("position %d = %d\n", i+1, value);
	}
	va_end(arg_ptr);
}

指针 #

指针是一种保存变量地址的变量,当 & 作为一元运算符时为取地址符。指针的定义也很有意思,星号 * 不是定义在类型上,而是定义在变量上:

int *ip; // ip 是指向 int 类型的指针
int *f(); // f 是一个函数,它返回一个指向 int 类型的指针
void *p; // p 是一个指向任意类型的指针

以前也没觉得别扭,现在有了 Go 语言的基础还是觉得 Go 更严谨一些。。

指针在函数中的使用举例如下:

void swap(int *px, int *py){
  int temp;
  
  temp = *px;
  *px = *py;
  *py = tem;
}

在函数的形参中使用指针可以避免 C 语言传值的参数传递方式所带来的副本复制问题。

对于 C 语言来说,当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。

结构体(struct) #

结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便使用关键字 struct 将这些变量组织在一个名字之下。

struct point {
  int x;
  int y;
};

struct point pt;
struct point maxpt = {320, 300};


void main() {
  printf("%d,%d", pt.x, pt.y);
}

可以定义一个指向结构体的指针 struct point *p,并通过 (*p).x 访问结构成员,同时为了方便,结构体指针还可以用 p->x 进行访问

结构体可以嵌套。

结构标记 point 是可选的:

struct {
  int x;
  int y;
} pt;

这里直接定义了一个结构体变量 pt

类型定义(typedef) #

C 语言提供了一个称为 typedef 的功能,用来建立新的数据类型名,例如:

typedef int Length

将 Length 定义为与 int 具有同等意义的名字。类型 Length 可用于类型声明、类型转换等,它和 int 完全相同。

联合(union) #

位字段 #

枚举(enum) #

ANSI C 还支持枚举类型,该语言类型经过了长期的发展才形成。

enum DAY{
  MON=1, TUE, WED, THU, FRI, SAT, SUN
};

enum DAY day;

参考 #

C 程序设计语言

C 语言标准 https://en.wikipedia.org/wiki/ANSI_C

ANSI 美国国家标准协会 American National Standards Institute

ASCII(American Standard Code for Information Interchange)美国信息交换标准代码

本文共 3505 字,上次修改于 Dec 5, 2024
相关标签: Linux, Clang