C的基本语法、内容
前言
省略了一些基本语法
一些关键字
const
定义常量,不能被改变
break
跳出当前循环
enum
声明枚举类型
extern
告诉编译器这个变量/函数的定义在其他文件中
goto
无条件跳转
sizeof
计算某数据类型或变量的所占字节数
static
静态变量,控制变量或函数的作用域、生命周期
typedef
给数据类型起别名
不同标准关键字有些许不同,C99,C11均新增了一些关键字。
数据类型
不仅int
,char
等类型,自定义结构体也是数据类型。
数据类型的存储大小和系统、编译器有关, 如int
可能是2或者4字节,即2^15,2^31范围大小(第一位符号位),long
4字节。 例如本机上运行sizeof(int)
结果是4
。
浮点类型不仅有大小还有精度。
void类型:返回值,函数参数可以接收一个void,void类指针。
变量
float类型有4个字节,32bit,其中,第一位为符号位,接下来8位为指数,剩余23位是小数, double是双精度,其为第一位符号位,接下来11位为指数,接下来52位小数, 这其中可以引出一个问题,为什么有时候两个小数相加末尾会多出0000000003
此类, 这是因为小数在表示的时候就有精度丢失。
变量在使用之前应该初始化,不初始化的变量其值是未定义的。
对于全局变量和静态变量(在函数内部定义的静态变量和在函数外部定义的全局变量), 以下是不同类型的变量在没有显式初始化时的默认值:
- 整型变量(int、short、long等):默认值为0。
- 浮点型变量(float、double等):默认值为0.0。
- 字符型变量(char):默认值为’\0’,即空字符。
- 指针变量:默认值为NULL,表示指针不指向任何有效的内存地址。
- 数组、结构体、联合等复合类型的变量: 它们的元素或成员将按照相应的规则进行默认初始化,这可能包括对元素递归应用默认规则。
需要注意的是,局部变量(在函数内部定义的非静态变量)不会自动初始化为默认值,它们的初始值是未定义的(包含垃圾值)。因此,在使用局部变量之前,应该显式地为其赋予一个初始值。
总结起来,C 语言中变量的默认值取决于其类型和作用域。全局变量和静态变量的默认值为0,字符型变量的默认值为 \0,指针变量的默认值为 NULL,而局部变量没有默认值,其初始值是未定义的。
变量的声明、定义和初始化有所区别,例如前面提到的extern
关键字便是声明一个变量,即告诉编译器这个变量在另外一个地方,请编译的时候不要创建其存储空间(在实际的例如int a;
,这里,即声明并定义了)。
循环语法
C提供了for
,while
和do..while
三种循环语法,实际上,递归等也可以实现循环。 相关的,break
,continue
提供跳出循环和跳出当前步控制。
1
2
3
4
5
6
7
8
9
10
11
12
13
for ( init; condition; increment )
{
statement(s);
}
while(condition)
{
statement(s);
}
o
{
statement(s);
}while( condition );
do..while
后面记得加;
号。
函数和作用域
与前面变量声明定义相似,函数也可先声明,告诉编译器实际函数主体在其他地方定义。
1
return_type function_name( parameter list );
作用域:函数内部,外部,形参(形参和外部全局同名,优先形参,就近)。
数组
数组声明和定义:type arrayName [ arraySize ];
, 初始化:double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
, 访问第几个元素:double salary = balance[9];
。
获取数组长度:
1
2
int numbers[] = {1, 2, 3, 4, 5};
int length = sizeof(numbers) / sizeof(numbers[0]);
值得注意的是:数组名是地址,数组元素的地址是按顺序排列下去的,因此对地址的+1
等操作也可以访问数组元素,这点在指针中详讲。
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
int arr[5] = {1, 10, 100, 1000, 10000};
int i = 0;
for(i; i < 5; i++){
printf("%d\n", *(arr+i));
}
}
枚举
枚举更像一个语法糖,让代码更简洁,例如定义星期常量:
1
2
3
4
#define MON 1
#define TUE 2
#define WED 3
...
用枚举可以这么写:
1
2
3
4
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
值得注意的是:第一个枚举成员的默认值为整型的0
,后续枚举成员的值在前一个成员上加1
。我们在这个实例中把第一个枚举成员的值定义为1
,第二个就为2
,以此类推。如果THU
定义成10
,那FRI
默认为11
,即定义了前面一个,后面会默认+1
。
这里只是声明了枚举类型,实际还需定义:
1
2
3
4
5
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;#先声明,再定义
1
2
3
4
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;#声明并定义
1
2
3
4
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;#直接定义
这和结构体很类似。
字符串
在C语言中,字符串实际上是使用空字符\0
结尾的一维字符数组。因此\0
是用于标记字符串的结束。 空字符又称结束符,缩写NUL
,是一个数值为0
的控制字符,\0
是转义字符,意思是告诉编译器,这不是字符0
,而是空字符,例如:
1
2
char site[6] = {'A', 'P', 'P', 'L', 'E', '\0'};
char site[] = "APPLE";
一些常用的字符串处理函数: strcpy(s1, s2);
,复制字符串s2
到字符串s1
。strcat(s1, s2);
,连接字符串s2
到字符串s1
的末尾。,strlen(s1);
,返回字符串s1
的长度(这里返回的是实际长度)。,strcmp(s1, s2);
,如果s1
和s2
是相同的,则返回0
;如果s1<s2
则返回小于0
;如果s1>s2
则返回大于0
。,strchr(s1, ch);
,返回一个指针,指向字符串s1
中字符ch
的第一次出现的位置。,strstr(s1, s2);
,返回一个指针,指向字符串s1
中字符串s2
的第一次出现的位置。
typedef
C语言提供了typedef
关键字,您可以使用它来为类型取一个新的名字。为单字节数字定义了一个术语BYTE
: typedef unsigned char BYTE;
, 使用BYTE b1, b2;
,
#define
是C指令,用于为各种数据类型定义别名,与typedef
类似,但是它们有以下几点不同:
typedef
仅限于为类型定义符号名称,#define
不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义1
为ONE
。typedef
是由编译器执行解释的,#define
语句是由预编译器进行处理的(预处理会替换,gcc编译四个过程讲过)。
预处理器和宏
预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把C预处理器(C Preprocessor)简写为CPP。
#define
定义宏
#include
包含一个源代码文件
#undef
取消已定义的宏
#ifdef
如果宏已经定义,则返回真
#ifndef
如果宏没有定义,则返回真
#if
如果给定条件为真,则编译下面代码
#else
#if
的替代方案
#elif
如果前面的#if
给定条件不为真,当前条件为真,则编译下面代码
#endif
结束一个#if……#else
条件编译块
#error
当遇到标准错误时,输出错误消息
#pragma
使用标准化方法,向编译器发布特殊的命令到编译器中
如告诉CPP取消已定义的FILE_SIZE
,并定义它为42
。:
1
2
#undef FILE_SIZE
#define FILE_SIZE 42
如告诉CPP只有当MESSAGE
未定义时,才定义MESSAGE
。:
1
2
3
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endif
如告诉CPP如果定义了DEBUG
,则执行处理语句。
1
2
3
#ifdef DEBUG
/* Your debugging statements here */
#endif
标准也定义了一些宏供使用:
__DATE__
当前日期,一个以"MMM DD YYYY"
格式表示的字符常量。
__TIME__
当前时间,一个以"HH:MM:SS"
格式表示的字符常量。
__FILE__
这会包含当前文件名,一个字符串常量。
__LINE__
这会包含当前行号,一个十进制常量。
__STDC__
当编译器以ANSI标准编译时,则定义为1
。
宏一行写不下,可以用宏延续运算符\
。
字符串常量化运算符#
,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符#
:
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
int main(void)
{
message_for(Carole, Debra);
return 0;
}
#output: Carole and Debra: We love you!
标记粘贴运算符##
,宏定义内的标记粘贴运算符##
会合并两个参数。:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
int main(void)
{
int token34 = 40;
tokenpaster(34);
return 0;
}
# output: token34 = 40
defined()
运算符,预处理器defined
运算符是用在常量表达式中的,用来确定一个标识符是否已经使用#define
定义过。:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#if !defined (MESSAGE)
#define MESSAGE "You wish!"
#endif
int main(void)
{
printf("Here is the message: %s\n", MESSAGE);
return 0;
}
# output: Here is the message: You wish!
头文件
头文件是扩展名为.h
的文件,包含了C函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。在程序中要使用头文件,需要使用C预处理指令#include
来引用它。
只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
1
2
3
4
5
6
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
这种结构就是通常所说的包装器#ifndef
。当再次引用头文件时,条件为假,因为HEADER_FILE
已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。如下:
1
2
3
4
5
6
7
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif
但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为#include
的直接参数,您只需要使用宏名称代替即可:
1
2
3
#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H
强制类型转换
强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个long
类型的值到一个简单的整型中,您需要把long
类型强制转换为int
类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如:(type_name) expression
。
强制类型转换运算符的优先级大于运算符,类型转换可以是隐式的,由编译器自动执行,如用浮点数除整数,结果是浮点数,不过良好的编程习惯是有需要类型转换的时候都用上强制类型转换运算符。
(怎么获取一个变量的数据类型?)
结构体
结构体定义由关键字struct
和结构体名组成,结构体名可以根据需要自行定义。
struct
语句定义了一个包含多个成员的新的数据类型,struct
语句的格式如下:
1
2
3
4
5
6
struct tag {
member-list
member-list
member-list
...
} variable-list ;
其中,tag
是结构体标签。
member-list
是标准的变量定义,比如int i
; 或者float f
;,或者其他有效的变量定义。
variable-list
结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。
一般来说,stuct的写法以上三个要出现两个,和枚举类似,使用typedef
有多种写法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct
{
int a;
char b;
double c;
} s1;
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
int a;
char b;
double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
//也可以用typedef创建新类型
typedef struct
{
int a;
char b;
double c;
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
结尾记得加;
号。
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct B; //对结构体B进行不完整声明
//结构体A中包含指向结构体B的指针
struct A
{
struct B *partner;
//other members;
};
//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
struct A *partner;
//other members;
};
访问结构体成员用.
,访问指针成员用->
。
对于结构体,sizeof
将返回结构体的总字节数,包括所有成员变量的大小以及可能的填充字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person;
printf("结构体 Person 大小为: %zu 字节\n", sizeof(person));
return 0;
}
// output: 28
// 20+4+4
指针
简单来说,每个变量都存在地址为xxx的内存中,因此衍生出&
和*
符号,分别代表取地址和取值。
如数组中提到的arr
即时数组变量名也是数组第一个元素地址,因此可以通过*arr
获取第一个元素值。
命名
换句话说,所以如果定义int a;
,显然不知道这个a
代表想声明一个地址变量还是整型变量,为此引入写法: type * _name_
, 或者说type* _name_
, 这样就好理解了,在这里,type*
代表想声明个什么类型变量的地址。 但重要的是这意味着不要在乎它的类型,地址怎么在乎类型?地址都是一串十六进制数,void * _name_
就代表一个指针,不要管前面的void *
,它就是一个简单的指针,但是,对于编译器来说,假设定义void *a = 10; *a += 10;
,编译器不知道这里10
是什么,是整数还是什么呢? 因此不管类型如何,实际代表都是一个十六进制的地址,只是代表的数据类型不一样(这只是一个语法,所以区分这里的*
和取值*
,这仅仅只是个语法而已)。
使用vs
等IDE可以查看内存中地址实际对应的值,设声明定义了int *a = 10;
,则在&a
这个地址上连续分配了四个字节存储了10
这个数,即0a 00 00 00
,这里是16进制。
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个NULL
值是一个良好的编程习惯。赋为NULL
值的指针被称为空指针。NULL
指针是一个定义在标准库中的值为0x0
的常量。
指针数组
同样的,type * _name_[]
可以声明定义指针数组。
例如int *a[3]
,这意味着3个整型类型指针。
指向指针的指针
即多个*
声明定义指针变量。int*
代表声明了一个int
类型的指针,则int* *
代表声明了一个int*
类型的指针。
内存管理
C语言为内存的分配和管理提供了几个函数。这些函数可以在<stdlib.h>
头文件中找到。
在C语言中,指针是一个变量,它存储了一个内存地址,这个内存地址可以指向任何数据类型的变量,包括整数、浮点数、字符和数组等。因此C语言提供了一些函数和运算符,使得程序员可以对内存进行操作,包括分配、释放、移动和复制等。
void *calloc(int num, int size);
在内存中动态地分配num
个长度为size
的连续空间,并将每一个字节都初始化为0
。所以它的结果是分配了num*size
个字节长度的内存空间,并且每个字节的值都是0
。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。
void free(void *address)
;
该函数释放address
所指向的内存块,释放的是动态分配的内存空间。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态。
void *malloc(int num);
在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。
void *realloc(void *address, int newsize);
该函数重新分配内存,把内存扩展到newsize
。用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。
注意:void *
类型表示未确定类型的指针。C、C++规定void *
类型可以通过类型转换强制转换为任何其它类型的指针。
如果预先不知道需要存储的文本长度,可以定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存。
一般地,对应每个malloc
调用,应该调用一次free
。
所以从常用的malloc
的参数等可知其写法:
1
2
3
int *p;
p = (int *)malloc(sizeof(int));
即分配一个int
大小的内存空间,并且类型是int *
,同时函数返回地址赋给p
。
1
2
b = (int *)malloc(1);
*b = 200;
同样的,上述代码可能会导致内存访问越界和未定义行为,因为200
占4个字节,但是只分配了1个字节。