C语言复习

第一步

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
/* 我的第一个 C 程序 */
printf("Hello, World! \n");
return 0;
}
  1. 打开一个文本编辑器,添加上述代码。
  2. 保存文件为 hello.c
  3. 打开命令提示符,进入到保存文件所在的目录。
  4. 键入 gcc hello.c,输入回车,编译代码。
  5. 如果代码中没有错误,命令提示符会跳到下一行,并生成 a.out 可执行文件。
  6. 现在,键入 a.out 来执行程序。
  7. 您可以看到屏幕上显示 “Hello World”
1
2
3
$ gcc hello.c
$ ./a.out
Hello, World!

数据类型

整数类型

下表列出了关于标准整数类型的存储大小和值范围的细节:

类型 存储大小 值范围
char 1 字节 -128 到 127 或 0 到 255
unsigned char 1 字节 0 到 255
signed char 1 字节 -128 到 127
int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
short 2 字节 -32,768 到 32,767
unsigned short 2 字节 0 到 65,535
long 4 字节 -2,147,483,648 到 2,147,483,647
unsigned long 4 字节 0 到 4,294,967,295

注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。

以下列出了32位系统与64位系统的存储大小的差别(windows 相同)

浮点类型

下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:

类型 存储大小 值范围 精度
float 4 字节 1.2E-38 到 3.4E+38 6 位小数
double 8 字节 2.3E-308 到 1.7E+308 15 位小数
long double 16 字节 3.4E-4932 到 1.1E+4932 19 位小数

头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间以及它的范围值:

  • ** a,默认为10进制 ,10 ,20。
  • ** b,以0开头为8进制,045,021。
  • ** c.,以0b开头为2进制,0b11101101。
  • ** d,以0x开头为16进制,0x21458adf。

void 类型

void 类型指定没有可用的值。它通常用于以下三种情况下:

序号 类型与描述
1 函数返回为空C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
2 函数参数为空C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
3 指针指向 void类型为 void 的指针代表对象的地址,而不是类型。例如,内存分配函数 **void \malloc( size_t size );** 返回指向 void 的指针,可以转换为任何数据类型。

如果现在您还是无法完全理解 void 类型,不用太担心,在后续的章节中我们将会详细讲解这些概念。

位运算符

假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:

A = 0011 1100

B = 0000 1101

-—————-

A&B = 0000 1100

A|B = 0011 1101

A^B = 0011 0001

~A = 1100 0011

下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:

运算符 实例 描述
& (A & B) 将得到 12,即为 0000 1100 按位与操作,按二进制位进行”与”运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
/ (A \ B) 将得到 61,即为 0011 1101 按位或运算符,按二进制位进行”或”运算。运算规则:0/0=0; 0/1=1; 1/0=1; 1/1=1;
^ (A ^ B) 将得到 49,即为 0011 0001 异或运算符,按二进制位进行”异或”运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
~ (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 取反运算符,按二进制位进行”取反”运算。运算规则:~1=0; ~0=1;
<< A << 2 将得到 240,即为 1111 0000 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
>> A >> 2 将得到 15,即为 0000 1111 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

存储类

auto 存储类

auto 存储类是所有局部变量默认的存储类。

1
2
3
4
{
int mount;
auto int month;
}

上面的实例定义了两个带有相同存储类的变量,auto 只能用在函数内,即 auto 只能修饰局部变量。

register 存储类

register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。

1
2
3
{
register int miles;
}

寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。

static 存储类

static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。

static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。

全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。

extern 存储类

extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 ‘extern’ 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

内存管理

本章将讲解 C 中的动态内存管理。C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。

序号 函数和描述
1 void *calloc(int num, int size);在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
2 void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
3 void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
4 void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize

注意:void 类型表示未确定类型的指针。C、C++ 规定 void 类型可以通过类型转换强制转换为任何其它类型的指针。

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
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
char name[100];
char *description;

strcpy(name, "Zara Ali");

/* 动态分配内存 */
description = (char *)malloc( 30 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student.");
}
/* 假设您想要存储更大的描述信息 */
description = realloc( description, 100 * sizeof(char) );
if( description == NULL )
{
fprintf(stderr, "Error - unable to allocate required memory\n");
}
else
{
strcat( description, "She is in class 10th");
}

printf("Name = %s\n", name );
printf("Description: %s\n", description );

/* 使用 free() 函数释放内存 */
free(description);
}

常量

字符串

C 中有大量操作字符串的函数:

序号 函数 & 目的
1 strcpy(s1, s2);复制字符串 s2 到字符串 s1。
2 strcat(s1, s2);连接字符串 s2 到字符串 s1 的末尾。
3 strlen(s1);返回字符串 s1 的长度。
4 strcmp(s1, s2);如果 s1 和 s2 是相同的,则返回 0;如果 s1s2 则返回大于 0。
5 strchr(s1, ch);返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
6 strstr(s1, s2);返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

下面的实例使用了上述的一些函数:

预定义宏

ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
DATE 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
TIME 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
FILE 这会包含当前文件名,一个字符串常量。
LINE 这会包含当前行号,一个十进制常量。
STDC 当编译器以 ANSI 标准编译时,则定义为 1。

让我们来尝试下面的实例:

预处理器运算符

C 预处理器提供了下列的运算符来帮助您创建宏:

宏延续运算符(\)

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

1
2
#define  message_for(a, b)  \
printf(#a " and " #b ": We love you!\n")
字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:

1
2
3
4
5
6
7
8
9
10
#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;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
Carole and Debra: We love you!
标记粘贴运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#define tokenpaster(n) printf ("token" #n " = %d", token##n)

int main(void)
{
int token34 = 40;

tokenpaster(34);
return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
token34 = 40

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

1
printf ("token34 = %d", token34);

这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)标记粘贴运算符(##)

defined() 运算符

预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#if !defined (MESSAGE)
#define MESSAGE "You wish!"
#endif

int main(void)
{
printf("Here is the message: %s\n", MESSAGE);
return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
Here is the message: You wish!
参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

1
2
3
int square(int x) {
return x * x;
}

我们可以使用宏重写上面的代码,如下:

1
#define square(x) ((x) * (x))

在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define MAX(x,y) ((x) > (y) ? (x) : (y))

int main(void)
{
printf("Max between 20 and 10 is %d\n", MAX(10, 20));
return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
Max between 20 and 10 is 20

函数

可变参数

有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。下面的实例演示了这种函数的定义。

int func(int, … ) { . . .} int main(){ func(2, 2, 3); func(3, 2, 3, 4);}

请注意,函数 func() 最后一个参数写成省略号,即三个点号(),省略号之前的那个参数是 int,代表了要传递的可变参数的总数。为了使用这个功能,您需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏。具体步骤如下:

  • 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
  • 在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
  • 使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
  • 使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
  • 使用宏 va_end 来清理赋予 va_list 变量的内存。

现在让我们按照上面的步骤,来编写一个带有可变数量参数的函数,并返回它们的平均值:

实例

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
#include <stdio.h>
#include <stdarg.h>

double average(int num,...)
{

va_list valist;
double sum = 0.0;
int i;

/* 为 num 个参数初始化 valist */
va_start(valist, num);

/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++)
{
sum += va_arg(valist, int);
}
/* 清理为 valist 保留的内存 */
va_end(valist);

return sum/num;
}

int main()
{
printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}

当上面的代码被编译和执行时,它会产生下列结果。应该指出的是,函数 average() 被调用两次,每次第一个参数都是表示被传的可变参数的总数。省略号被用来传递可变数量的参数。

1
2
Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000

命令行参数

执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。

命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。下面是一个简单的实例,检查命令行是否有提供参数,并根据参数执行相应的动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main( int argc, char *argv[] )
{
if( argc == 2 )
{
printf("The argument supplied is %s\n", argv[1]);
}
else if( argc > 2 )
{
printf("Too many arguments supplied.\n");
}
else
{
printf("One argument expected.\n");
}
}

使用一个参数,编译并执行上面的代码,它会产生下列结果:

1
2
$./a.out testing
The argument supplied is testing

使用两个参数,编译并执行上面的代码,它会产生下列结果:

1
2
$./a.out testing1 testing2
Too many arguments supplied.

不传任何参数,编译并执行上面的代码,它会产生下列结果:

指针

指针的算术运算

C 指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。

假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:

1
ptr++

在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 个字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。

指向指针的指针

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。例如,下面声明了一个指向 int 类型指针的指针:

1
int **var;

当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main ()
{
int var;
int *ptr;
int **pptr;

var = 3000;

/* 获取 var 的地址 */
ptr = &var;

/* 使用运算符 & 获取 ptr 的地址 */
pptr = &ptr;

/* 使用 pptr 获取值 */
printf("Value of var = %d\n", var );
printf("Value available at *ptr = %d\n", *ptr );
printf("Value available at **pptr = %d\n", **pptr);

return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
Value of var = 3000
Value available at *ptr = 3000
Value available at **pptr = 3000

传递指针给函数

C 语言允许您传递指针给函数,只需要简单地声明函数参数为指针类型即可。

下面的实例中,我们传递一个无符号的 long 型指针给函数,并在函数内改变这个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <time.h>

void getSeconds(unsigned long *par);

int main ()
{
unsigned long sec;
getSeconds( &sec );
/* 输出实际值 */
printf("Number of seconds: %ld\n", sec );
return 0;
}

void getSeconds(unsigned long *par)
{
/* 获取当前的秒数 */
*par = time( NULL );
return;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
Number of seconds :1294450468

能接受指针作为参数的函数,也能接受数组作为参数,如下所示:

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
31
32
33
34
#include <stdio.h>

/* 函数声明 */
double getAverage(int *arr, int size);

int main ()
{
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;

/* 传递一个指向数组的指针作为参数 */
avg = getAverage( balance, 5 ) ;

/* 输出返回值 */
printf("Average value is: %f\n", avg );

return 0;
}

double getAverage(int *arr, int size)
{
int i, sum = 0;
double avg;

for (i = 0; i < size; ++i)
{
sum += arr[i];
}

avg = (double)sum / size;

return avg;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
Average value is: 214.40000

函数指针

函数指针是指向函数的指针变量。

通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。

函数指针可以像一般函数一样,用于调用函数、传递参数。

函数指针变量的声明:

1
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
int main(void)
{
/* p 是函数指针 */
int (* p)(int, int) = & max; // &可以省略
int a, b, c, d;

printf("请输入三个数字:");
scanf("%d %d %d", & a, & b, & c);

/* 与直接调用函数等价,d = max(max(a, b), c) */
d = p(p(a, b), c);

printf("最大的数字是: %d\n", d);

return 0;
}

以下实例声明了函数指针变量 p,指向函数 max:

编译执行,输出结果如下:

1
2
请输入三个数字:1 2 3
最大的数字是: 3

回调函数

函数指针作为某个函数的参数

函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。

简单讲:回调函数是由别人的函数执行时调用你实现的函数。

以下是自知乎作者常溪玲的解说:

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

实例

实例中 populate_array 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。

实例中我们定义了回调函数 getNextRandomValue,它返回一个随机值,它作为一个函数指针传递给 populate_array 函数。

populate_array 将调用 10 次回调函数,并将回调函数的返回值赋值给数组。

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
#include <stdlib.h>  
#include <stdio.h>

// 回调函数
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}

// 获取随机值
int getNextRandomValue(void)
{
return rand();
}

int main(void)
{
int myarray[10];
populate_array(myarray, 10, getNextRandomValue);
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}

编译执行,输出结果如下:

1
16807 282475249 1622650073 984943658 1144108930 470211272 101027544 1457850878 1458777923 2007237709

数组名arr通常情况下代表数组元素的首地址

1
2
printf("%d\n",arr);
//输出为1

数组名arr在两种情况表示整个数组(即不表示元素首地址)
1、在定义数组的同一个函数中,求sizeof(arr),整个数组的字节数
2、在定义数组的同一个函数中,&arr+1,加整个数组