C语言RUNOOB教程


C语言RUNOOB教程


正文

C 语言教程

C 语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。

当前最新的 C 语言标准为 C18 ,在它之前的 C 语言标准有 C17、C11…C99 等。

#include<stdio.h>  

/*
这一句话是必须要的格式
stdio 表示系统文件库, 也可以声明其它的
.h  表示头文件,因为这些文件都是放在程序各文件的开头
#include 告诉预处理器将指定头文件的内容插入到预处理器命令的相应位  导入头文件的预编译指令
<> 表示系统自带的库
也可以写成" " 表示用户自定义的库
如果写成" "并且自定义的库里面没有这个文件系统会自动查找自带的库,如果还是没有报错
*/

int main() // 程序的入口
{   //程序从这里开始运行
    /*
    int 表示数字格式,返回一个数字
    main()主函数 表示程序的入口,一个程序有且只能有一个main函数的存在
    */
    printf("hello C"); //打印一个hello C
    
    return 0; //返回一个整数0,因为它是int类型,所以只能返回整数
}  //程序从这里结束

所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。

/* ... */ 用于注释说明。

printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声明。

stdio.h 是一个头文件 (标准输入输出头文件) , #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。

return 0; 语句用于表示退出程序。

C 简介

C 语言是一种通用的高级语言,最初是由丹尼斯·里奇在贝尔实验室为开发 UNIX 操作系统而设计的。 C 语言最开始是于 1972 年在 DEC PDP-11 计算机上被首次实现。

在 1978 年,布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)制作了 C 的第一个公开可用的描述,现在被称为 K&R 标准。

UNIX 操作系统,C编译器,和几乎所有的 UNIX 应用程序都是用 C 语言编写的。由于各种原因,C 语言现在已经成为一种广泛使用的专业语言。

C 环境设置

文本编辑器

C 编译器

gcc 进行 c 语言编译分为四个步骤:

1.预处理,生成预编译文件(.i 文件):

gcc –E hello.c –o hello.i

2.编译,生成汇编代码(.s 文件):

gcc –S hello.i –o hello.s

3.汇编,生成目标文件(.o 文件):

gcc –c hello.s –o hello.o

4.链接,生成可执行文件:

gcc hello.o –o hello

有时候,进行调试,可能会用到某个步骤哦

C 程序结构

C 程序主要包括以下部分:

预处理器指令
函数
变量
语句 & 表达式
注释
#include <stdio.h>
 
int main()
{
   /* 我的第一个 C 程序 */
   printf("Hello, World! \n");
   
   return 0;
}

程序的第一行 #include <stdio.h> 是预处理器指令,告诉 C 编译器在实际编译之前要包含 stdio.h 文件。

下一行 int main() 是主函数,程序从这里开始执行。

下一行 /*...*/ 将会被编译器忽略,这里放置程序的注释内容。它们被称为程序的注释。

下一行 printf(...) 是 C 中另一个可用的函数,会在屏幕上显示消息 “Hello, World!”。

下一行 return 0; 终止 main() 函数,并返回值 0。

编译 & 执行 C 程序

gcc 命令如果不指定目标文件名时默认生成的可执行文件名为 a.out(linux) 或 a.exe(windows)。

可用 gcc [源文件名] -o [目标文件名] 来指定目标文件路径及文件名。

例如,windows 系统上,gcc hello.c -o target/hello 会在 target 目录下生成 hello.exe 文件(Linux 系统生成 hello 可执行文件), target 目录必须已存在,[源文件名]-o [目标文件名] 的顺序可互换, gcc -o target/hello hello.c 依然有效。

C 基本语法

C 的令牌(Token)

C 程序由各种令牌组成,令牌可以是关键字、标识符、常量、字符串值,或者是一个符号。例如,下面的 C 语句包括五个令牌:

printf("Hello, World! \n");

这五个令牌分别是:

printf
(
"Hello, World! \n"
)
;

注释

// 单行注释
/* 单行注释 */

/* 
 多行注释
 多行注释
 多行注释
 */

您不能在注释内嵌套注释,注释也不能出现在字符串或字符值中。

标识符

标识符:在编程语言中,标识符是用户编程时使用的名字,变量、常量、函数、语句块都有名字。 是用来标识某个实体的一个符号,是对变量名、函数名、标号和其他各种用户定义的对象命名。

C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始, 后跟零个或多个字母、下划线和数字(0-9)。

C 标识符内不允许出现标点字符,比如 @、$ 和 %。C 是区分大小写的编程语言。

C语言中标识符的命名规范:

  1. 标识符由字母、数字、下划线组成,并且首字母不能是数字。
  2. 不能把C的关键字作为用户的标识符,例如:if、for、while等。(注:标识符不能和C语言的关键字相同,也不能和用户自定义的函数或C语言库函数同名)
  3. 标识符长度是由机器上的编译系统决定的,一般的限制为8字符,(注:8字符长度限制是C89标准,C99标准已经扩充长度,其实大部分工业标准都更长)。
  4. 标识符对大小写敏感,即严格区分大小写。一般对变量名用小写,符号常量命名用大写。(注:C语言中字母是区分大小写的,因此score、Score、SCORE分别代表三个不同的标识符)
  5. 标识符命名应做到”见名知意”,例如,长度(外语:length),求和、总计(外语:sum),圆周率(外语:pi)

关键字

下表列出了 C 中的保留字。这些保留字不能作为常量名、变量名或其他标识符名称。

关键字 说明
auto 声明自动变量
break 跳出当前循环
case 开关语句分支
char 声明字符型变量或函数返回值类型
const 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变
continue 结束当前循环,开始下一轮循环
default 开关语句中的”其它”分支
do 循环语句的循环体
double 声明双精度浮点型变量或函数返回值类型
else 条件语句否定分支(与 if 连用)
enum 声明枚举类型
extern 声明变量或函数是在其它文件或本文件的其他位置定义
float 声明浮点型变量或函数返回值类型
for 一种循环语句
goto 无条件跳转语句
if 条件语句
int 声明整型变量或函数
long 声明长整型变量或函数返回值类型
register 声明寄存器变量
return 子程序返回语句(可以带参数,也可不带参数)
short 声明短整型变量或函数
signed 声明有符号类型变量或函数
sizeof 计算数据类型或变量长度(即所占字节数)
static 声明静态变量
struct 声明结构体类型
switch 用于开关语句
typedef 用以给数据类型取别名
unsigned 声明无符号类型变量或函数
union 声明共用体类型
void 声明函数无返回值或无参数,声明无类型指针
volatile 说明变量在程序执行中可被隐含地改变
while 循环语句的循环条件

C99 新增关键字

_Bool _Complex _Imaginary inline restrict

C11 新增关键字

Alignas _Alignof _Atomic _Generic _Noreturn _Static_assert _Thread_local

C 中的空格

在 C 中,空格用于描述空白符、制表符、换行符和注释。空格分隔语句的各个部分, 让编译器能识别语句中的某个元素(比如 int)在哪里结束,下一个元素在哪里开始。

int age;

在这里,int 和 age 之间必须至少有一个空格字符(通常是一个空白符),这样编译器才能够区分它们。

fruit = apples + oranges;

fruit 和 =,或者 = 和 apples 之间的空格字符不是必需的,但是为了增强可读性,您可以根据需要适当增加一些空格。

C 数据类型

以下列出了32位系统与64位系统的存储大小的差别:

为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 sizeof 运算符。 表达式 sizeof(type) 得到对象或类型的存储字节大小。

#include <stdio.h>
#include <limits.h>
 
int main()
{
   printf("int 存储大小 : %lu \n", sizeof(int));
   
   return 0;
}

%lu 为 32 位无符号整数。

浮点类型

float 单精度浮点值。单精度是这样的格式,1位符号,8位指数,23位小数。 1.2E-38 到 3.4E+38 , 6 位小数

double 双精度浮点值。双精度是1位符号,11位指数,52位小数。 2.3E-308 到 1.7E+308 , 15 位小数

void 类型

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

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

数据类型转换

1、数据类型转换:C 语言中如果一个表达式中含有不同类型的常量和变量,在计算时,会将它们自动转换为同一种类型; 在 C 语言中也可以对数据类型进行强制转换;

2、自动转换规则:

a)浮点数赋给整型,该浮点数小数被舍去;
b)整数赋给浮点型,数值不变,但是被存储到相应的浮点型变量中; 

3、强制类型转换形式: (类型说明符)(表达式)

实例程序:

#include<stdio.h>

int main()
{
    float f,x=3.6,y=5.2;
    int i=4,a,b;
    a=x+y;
    b=(int)(x+y);
    f=10/i;
    printf("a=%d,b=%d,f=%f,x=%f\n",a,b,f,x);
}

例中先计算 x+y 值为 8.8,然后赋值给 a,因为a为整型,所以自取整数部分8,a=8;

接下来 b 把 x+y 强制转换为整型;

最后 10/i 是两个整数相除,结果仍为整数 2,把 2 赋给浮点数 f;

x 为浮点型直接输出。

C 变量

变量其实只不过是程序可操作的存储区的名称。 C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。

变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C 是大小写敏感的。

C 中的变量定义

变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。

格式,type 必须是一个有效的 C 数据类型,variable_list 可以由一个或多个标识符名称组成,多个标识符之间用逗号分隔:

type variable_list;
type variable_name = value;

如:

int    i, j, k;
char   c, ch;
float  f, salary;
double d;

extern int d = 3, f = 5;    // d 和 f 的声明与初始化
int d = 3, f = 5;           // 定义并初始化 d 和 f
byte z = 22;                // 定义并初始化 z
char x = 'x';               // 变量 x 的值为 'x'

int i, j, k; 声明并定义了变量 i、j 和 k,这指示编译器创建类型为 int 的名为 i、j、k 的变量。

变量可以在声明的时候被初始化,如 行 int d = 3, f = 5;

不带初始化的定义:带有静态存储持续时间的变量会被隐式初始化为 NULL(所有字节的值都是 0)。

解说1

extern int a;     // 声明一个全局变量 a
int a;            // 定义一个全局变量 a
extern int a =0;  // 定义一个全局变量 a 并给初值。一旦给予赋值,一定是定义,定义才会分配存储空间
int a =0;         //定义一个全局变量 a,并给初值

声明之后你不能直接使用这个变量,需要定义之后才能使用。

第四个等于第三个,都是定义一个可以被外部使用的全局变量,并给初值。

糊涂了吧,他们看上去可真像。但是定义只能出现在一处。也就是说,不管是 int a 还是 int a=0 都只能出现一次,而那个 extern int a 可以出现很多次。

当你要引用一个全局变量的时候,你就要声明 extern int a 这时候 extern 不能省略,因为省略了,就变成 int a 这是一个定义,不是声明。

解说2

一个变量一定要先初始化才可以使用,因为 c 语言中默认一个没有初始化的变量值是一个不可知的很大值。 如下面所示,a 没有初始化,打印出 a 的值是 1606422582。

#include <stdio.h>  
int main()   
{  
    int a;  
    printf("a的值是%d\n",a);  
    return 0;  
}  

解说3

变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。

变量声明:用于向程序表明变量的类型和名字。

定义也是声明,extern 声明不是定义。

定义也是声明:当定义变量时我们声明了它的类型和名字。

extern 声明不是定义:通过使用 extern 关键字声明变量名而不定义它。

注意:

变量在使用前就要被定义或者声明。

在一个程序中,变量只能定义一次,却可以声明多次。

定义分配存储空间,而声明不会。

全局变量和局部变量在内存中的区别

全局变量保存在内存的全局存储区中,占用静态的存储单元;局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

C语言经过编译之后将内存分为以下几个区域:

(1)栈(stack):由编译器进行管理,自动分配和释放,存放函数调用过程中的各种参数、局部变量、返回值以及函数返回地址。操作方式类似数据结构中的栈。

(2)堆(heap):用于程序动态申请分配和释放空间。C语言中的malloc和free,C++中的new和delete均是在堆中进行的。 正常情况下,程序员申请的空间在使用结束后应该释放,若程序员没有释放空间,则程序结束时系统自动回收。注意:这里的“堆”并不是数据结构中的“堆”。

(3)全局(静态)存储区:分为DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量; BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。 其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。

(4)文字常量区:存放常量字符串。程序结束后由系统释放。

(5)程序代码区:存放程序的二进制代码。

显然,C语言中的全局变量和局部变量在内存中是有区别的。C语言中的全局变量包括外部变量和静态变量,均是保存在全局存储区中,占用永久性的存储单元; 局部变量,即自动变量,保存在栈中,只有在所在函数被调用时才由系统动态在栈中分配临时性的存储单元。

#include <stdio.h>
#include <stdlib.h>

int k1 = 1;
int k2;
static int k3 = 2;
static int k4;

int main( )
{  
    static int m1=2, m2;
    int i=1;
    char *p;
    char str[10] = "hello";
    char *q = "hello";
    p = (char *)malloc( 100 );
    free(p);
    printf("栈区-变量地址  i:%p\n", &i);
    printf("                p:%p\n", &p);
    printf("              str:%p\n", str);
    printf("                q:%p\n", &q);
    printf("堆区地址-动态申请:%p\n", p);
    printf("全局外部有初值 k1:%p\n", &k1);
    printf("    外部无初值 k2:%p\n", &k2);
    printf("静态外部有初值 k3:%p\n", &k3);
    printf("    外静无初值 k4:%p\n", &k4);
    printf("  内静态有初值 m1:%p\n", &m1);
    printf("  内静态无初值 m2:%p\n", &m2);
    printf("文字常量地址    :%p, %s\n",q, q);
    printf("程序区地址      :%p\n",&main);
    
    return 0;
}

C 中的变量声明

变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。 变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。

变量的声明有两种情况:

  1. 一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
  2. 另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。

除非有extern关键字,否则都是变量的定义。

C 中的左值(Lvalues)和右值(Rvalues)

C 中有两种类型的表达式:

  1. 左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
  2. 右值(rvalue):术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。

变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。

总结:

  1. 当需要保存数据的时候,需要lvalues。
  2. 当需要读取数据的时候,需要rvalues。

lvalues 和 rvalues 角色的相互转换。

1、 根据表达式的上下文情况,lvalues 在需要 rvalues 的地方会自动转换为 rvalues。例如:

int n;
int m;
m = n+2; // 这个表达式里 n 是 rvalues

2、 rvalues 永远不能转换为 lvalues

C 常量

常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。

常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。

常量就像是常规的变量,只不过常量的值在定义后不能进行修改。

整数常量

整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

浮点常量

浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。

字符常量

字符常量是括在单引号中,例如,'x' 可以存储在 char 类型的简单变量中。

字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。

字符串常量

字符串字面值或常量是括在双引号 "" 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。

在 C 语言中,单引号与双引号是有很大区别的。

在 C 语言中没有专门的字符串类型,因此双引号内的字符串会被存储到一个数组中,这个字符串代表指向这个数组起始字符的指针;

而单引号中的内容是一个 char 类型,是一个字符,这个字符对应的是 ASCII 表中的序列值。

定义常量

在 C 中,有两种简单的定义常量的方式:

  1. 使用 #define 预处理器。
  2. 使用 const 关键字。
#define identifier value

const type variable = value;

const 声明常量要在一个语句内完成。

解说1

#define 是宏定义,它不能定义常量,但宏定义可以实现在字面意义上和其它定义常量相同的功能, 本质的区别就在于 #define 不为宏名分配内存,而 const 也不为常量分配内存,怎么回事呢, 其实 const 并不是去定义一个常量,而是去改变一个变量的存储类,把该变量所占的内存变为只读!

解说2

const 定义的是变量不是常量,只是这个变量的值不允许改变是常变量!带有类型。编译运行的时候起作用存在类型检查。

define 定义的是不带类型的常数,只进行简单的字符替换。在预编译的时候起作用,不存在类型检查。 1、两者的区别

(1) 编译器处理方式不同

#define 宏是在预处理阶段展开。
const 常量是编译运行阶段使用。

(2) 类型和安全检查不同

#define 宏没有类型,不做任何类型检查,仅仅是展开。
const 常量有具体的类型,在编译阶段会执行类型检查。

(3) 存储方式不同

#define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)
const常量会在内存中分配(可以是堆中也可以是栈中)。

(4) const 可以节省空间,避免不必要的内存分配。 例如:

#define NUM 3.14159 //常量宏
const doulbe Num = 3.14159; //此时并未将Pi放入ROM中 ......
double i = Num; //此时为Pi分配内存,以后不再分配!
double I= NUM; //编译期间进行宏替换,分配内存
double j = Num; //没有内存分配
double J = NUM; //再进行宏替换,又一次分配内存!

const 定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象 #define 一样给出的是立即数, 所以,const 定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而 #define 定义的常量在内存中有若干个拷贝。

(5) 提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量, 没有了存储与读内存的操作,使得它的效率也很高。

(6) 宏替换只作替换,不做计算,不做表达式求解;

宏预编译时就替换了,程序运行时,并不分配内存。

解说3

define 注意“边缘效应”,例:#define N 2+3, N 的值是 5。

double a;
a = (float)N/(float)2;

在编译时我们预想 a=2.5,实际打印结果是 3.5 原因是在预处理阶段,编译器将 a=N/2 处理成 a=2+3/2, 这就是 define 宏的边缘效应,所以我们应该写成 #define N (2+3)

C 存储类

存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C 程序中可用的存储类:

auto
register
static
extern

auto 是局部变量的默认存储类, 限定变量只能在函数内部使用;

register 代表了寄存器变量,不在内存中使用;

static 是全局变量的默认存储类,表示变量在程序生命周期内可见;

extern 表示全局变量,即对程序内所有文件可见;

auto 存储类

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

{
   int mount;
   auto int month;
}

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

register 存储类

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

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

static 存储类

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

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

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

extern 存储类

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

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

C 语言中全局变量、局部变量、静态全局变量、静态局部变量的区别

从作用域看:

1、全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。 当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。

2、静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在, 它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

3、局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在, 函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。

4、静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里, 即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

从分配内存空间看:

1、全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间

2、全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。 这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。 由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

1)静态变量会被放在程序的静态数据存储区(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2)变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。

从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。 把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。应予以注意。

Tips:

A.若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
C.设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题,因为他们都放在静态数据存储区,全局可见;
D.如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量(这样的函数被称为:带"内部存储器"功能的的函数)
E.函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。

C 运算符

算术运算符

运算符    描述    实例
+    把两个操作数相加    A + B 将得到 30
-    从第一个操作数中减去第二个操作数    A - B 将得到 -10
*    把两个操作数相乘    A * B 将得到 200
/    分子除以分母    B / A 将得到 2
%    取模运算符,整除后的余数    B % A 将得到 0
++    自增运算符,整数值增加 1    A++ 将得到 11
--    自减运算符,整数值减少 1    A-- 将得到 9

关系运算符

运算符    描述    实例
==    检查两个操作数的值是否相等,如果相等则条件为真。    (A == B) 为假。
!=    检查两个操作数的值是否相等,如果不相等则条件为真。    (A != B) 为真。
>    检查左操作数的值是否大于右操作数的值,如果是则条件为真。    (A > B) 为假。
<    检查左操作数的值是否小于右操作数的值,如果是则条件为真。    (A < B) 为真。
>=    检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。    (A >= B) 为假。
<=    检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。    (A <= B) 为真。

逻辑运算符

运算符    描述    实例
&&    称为逻辑与运算符。如果两个操作数都非零,则条件为真。    (A && B) 为假。
||    称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。    (A || B) 为真。
!    称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。    !(A && B) 为真。

位运算符

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

赋值运算符

运算符    描述    实例
=    简单的赋值运算符,把右边操作数的值赋给左边操作数    C = A + B 将把 A + B 的值赋给 C
+=    加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数    C += A 相当于 C = C + A
-=    减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数    C -= A 相当于 C = C - A
*=    乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数    C *= A 相当于 C = C * A
/=    除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数    C /= A 相当于 C = C / A
%=    求模且赋值运算符,求两个操作数的模赋值给左边操作数    C %= A 相当于 C = C % A
<<=    左移且赋值运算符    C <<= 2 等同于 C = C << 2
>>=    右移且赋值运算符    C >>= 2 等同于 C = C >> 2
&=    按位与且赋值运算符    C &= 2 等同于 C = C & 2
^=    按位异或且赋值运算符    C ^= 2 等同于 C = C ^ 2
|=    按位或且赋值运算符    C |= 2 等同于 C = C | 2

杂项运算符

运算符    描述    实例
sizeof()    返回变量的大小。    sizeof(a) 将返回 4,其中 a 是整数。
&    返回变量的地址。    &a; 将给出变量的实际地址。
*    指向一个变量。    *a; 将指向一个变量。
? :    条件表达式    如果条件为真 ? 则值为 X : 否则值为 Y

运算符优先级

类别      运算符     结合性 
后缀     () [] -> . ++ - -       从左到右 
一元      + - ! ~ ++ - - (type)* & sizeof      从右到左 
乘除      * / %     从左到右 
加减     + -      从左到右 
移位      << >>      从左到右 
关系     < <= > >=      从左到右 
相等      == !=      从左到右 
位与 AND     &      从左到右 
位异或 XOR      ^      从左到右 
位或 OR      |      从左到右 
逻辑与 AND     &&      从左到右 
逻辑或 OR      ||      从左到右 
条件     ?:      从右到左 
赋值      = += -= *= /= %=>>= <<= &= ^= |=     从右到左 
逗号      ,      从左到右 

对取余运算的说明

取余,也就是求余数,使用的运算符是 %。C 语言中的取余运算只能针对整数,也就是说, % 的两边都必须是整数,不能出现小数,否则编译器会报错。

另外,余数可以是正数也可以是负数,由 % 左边的整数决定:

如果 % 左边是正数,那么余数也是正数;
如果 % 左边是负数,那么余数也是负数;

运算符优先级

括号成员是老大; // 括号运算符 []() 成员运算符. ->

全体单目排老二; // 所有的单目运算符比如++、 –、 +(正)、 -(负) 、指针运算*、&

乘除余三,加减四; // 这个”余”是指取余运算即%

移位五,关系六; // 移位运算符:« » ,关系:> < >= <= 等

等与不等排行七; // 即 == 和 !=

位与异或和位或; // 这几个都是位运算: 位与(&)异或(^)位或(|)

“三分天下”八九十;

逻辑与,逻辑或; // 逻辑运算符: ||&&

十一十二紧挨着; // 注意顺序: 优先级(||) 底于 优先级(&&)

条件只比赋值高, // 三目运算符优先级排到 13 位只比赋值运算符和 “,” 高

逗号运算最低级! //逗号运算符优先级最低

运算符优先级2

初等运算符>单目运算符>算术运算符>关系运算符>逻辑运算符>条件运算符>赋值运算符

初等运算符有:()、[ ]、->、. (后两者均为结构体成员运算符);

单目运算符有:!、~、++、–、sizeof、&、*;

算术运算符有:*、/、+、-、«、»;

关系运算符有:<、<=、>、>=、==、!=、&、^、|;(此栏排列仍有优先级顺序哦);

逻辑运算符有:&&、||

条件运算符有:?:(即三目运算符);

赋值运算符有:=、+=、-=、*=、/=、%=、»=、«=;等

负数以其正值的补码形式表达

在计算机中,负数以其正值的补码形式表达

什么叫补码呢?这得从原码,反码说起。

原码:一个整数,按照绝对值大小转换成的二进制数,称为原码。

比如 00000000 00000000 00000000 00000101 是 5 的原码。

反码:将二进制数按位取反,所得的新二进制数称为原二进制数的反码。

取反操作指:原为 1,得 0;原为 0,得 1。(1 变 0; 0 变 1)

比如:将 00000000 00000000 00000000 00000101 每一位取反,得 11111111 11111111 11111111 11111010。

称:11111111 11111111 11111111 11111010是 00000000 00000000 00000000 00000101 的反码。

反码是相互的,所以也可称:

11111111 11111111 11111111 11111010 和00000000 00000000 00000000 00000101 互为反码。

补码:反码加1称为补码。

也就是说,要得到一个数的补码,先得到反码,然后将反码加上 1,所得数称为补码。

比如:00000000 00000000 00000000 00000101 的反码是:11111111 11111111 11111111 11111010。

那么,补码为:

11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011

所以,-5 在计算机中表达为:11111111 11111111 11111111 11111011。

C 判断

判断结构要求程序员指定一个或多个要评估或测试的条件,以及条件为真时要执行的语句(必需的)和条件为假时要执行的语句(可选的)。

C 语言把任何非零和非空的值假定为 true,把零或 null 假定为 false。

判断语句

语句    描述
if 语句    一个 if 语句 由一个布尔表达式后跟一个或多个语句组成。
if...else 语句    一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。
嵌套 if 语句    您可以在一个 if 或 else if 语句内使用另一个 if 或 else if 语句。
switch 语句    一个 switch 语句允许测试一个变量等于多个值时的情况。
嵌套 switch 语句    您可以在一个 switch 语句内使用另一个 switch 语句。

? : 运算符

条件运算符 ? :,可以用来替代 if…else 语句。

Exp1 ? Exp2 : Exp3;

? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个表达式的值。 如果 Exp1 为假,则计算 Exp3 的值,结果即为整个表达式的值。

C 循环

循环语句允许我们多次执行一个语句或语句组。

循环类型

循环类型    描述
while 循环    当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。
for 循环    多次执行一个语句序列,简化管理循环变量的代码。
do...while 循环    除了它是在循环主体结尾测试条件外,其他与 while 语句类似。
嵌套循环    您可以在 while、for 或 do..while 循环内使用一个或多个循环。

循环控制语句

控制语句    描述
break 语句    终止循环或 switch 语句,程序流将继续执行紧接着循环或 switch 的下一条语句。
continue 语句    告诉一个循环体立刻停止本次循环迭代,重新开始下次循环迭代。
goto 语句    将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句。

一定要注意 break 语句与 continue 语句的区别,前者是结束整个循环过程,后者这是结束本次循环。

无限循环

如果条件永远不为假,则循环将变成无限循环。for 循环在传统意义上可用于实现无限循环。 由于构成循环的三个表达式中任何一个都不是必需的,您可以将某些条件表达式留空来构成一个无限循环。

#include <stdio.h>
 
int main ()
{
   for( ; ; )
   {
      printf("该循环会永远执行下去!\n");
   }
   return 0;
}

当条件表达式不存在时,它被假设为真。您也可以设置一个初始值和增量表达式, 但是一般情况下,C 程序员偏向于使用 for(;;) 结构来表示一个无限循环。

C 函数

函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。

函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。

定义函数

return_type function_name( parameter list )
{
   body of the function
}

在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:

返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值, 在这种情况下,return_type 是关键字 void。

函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。

参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。 参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。

函数主体:函数主体包含一组定义函数执行任务的语句。

函数声明

函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

函数声明包括以下几个部分:

return_type function_name( parameter list );

在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:

int max(int, int);

当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。

调用函数

当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务, 当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。

调用函数时,传递所需参数,如果函数返回一个值,则可以存储返回值。

函数参数

如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。

形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。

当调用函数时,有两种向函数传递参数的方式:

调用类型    描述
传值调用    该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
引用调用    通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

默认情况下,C 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的实际参数。

解说

占位符

占位符就是先占住一个固定的位置,等着你再往里面添加内容的符号,广泛用于计算机中各类文档的编辑。

格式占位符(%)是在C/C++语言中格式输入函数,如 scanf、printf 等函数中使用。其意义就是起到格式占位的意思,表示在该位置有输入或者输出。

%d, %i 代表整数
%f 浮点
%s 字符串
%c char
%p 指针
%fL 长log
%e 科学计数
%g 小数或科学计数。
%a,%A 读入一个浮点值(仅C99有效)。
%c 读入一个字符。
%d 读入十进制整数。
%i 读入十进制,八进制,十六进制整数。
%o 读入八进制整数。
%x,%X 读入十六进制整数。
%s 读入一个字符串,遇空格、制表符或换行符结束。
%f,%F,%e,%E,%g,%G 用来输入实数,可以用小数形式或指数形式输入。
%p 读入一个指针。
%u 读入一个无符号十进制整数。
%n 至此已读入值的等价字符数。
%[] 扫描字符集合。
%% 读 % 符号

实例:

scanf("%d,%d,%d",&a,&b,&c); // 从键盘输入三个整数,用逗号分隔  
scanf("%c", &s);   // 从键盘输入一个字符  
scanf("%f", &f);   // 从键盘输入一个浮点型数据  
printf("%d\n",a);  // 输出一个整数  
printf("%f\n",b);  // 输出一个浮点数  
printf("%s\n",c);  // 输出一个字符, 其中\n表示换行

C 作用域规则

任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。

C 语言中有三个地方可以声明变量:

  • 在函数或块内部的局部变量
  • 在所有函数外部的全局变量
  • 在函数参数定义中的形式参数

局部变量

在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。

全局变量

全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的。

在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。

#include <stdio.h>
 
/* 全局变量声明 */
int g = 20;
 
int main ()
{
    /* 局部变量声明 */
    int g = 10;
    printf ("value of g = %d\n",  g);
    
    return 0;
}

输出:

value of g = 10

形式参数

函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。

初始化局部变量和全局变量

当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化

数据类型    初始化默认值
int     0
char     '\0'
float     0
double     0
pointer     NULL 

正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果, 因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。

解读

在调用函数过程中发生的实参与形参之间的数据传递,常称为“虚实结合”

在定义函数中制定的形参,在没有出现函数调用时不占用内存中的存储单元。在函数调用时才分配内存
将实参的值传递给形参
在执行函数时,由于形参已经有值。可以用形参进行运算。
通过return语句将函数值返回,若无返回值,则无return
调用结束后,形参被释放掉,实参保留原值(单向传值)

C 数组

C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。 数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。

所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

数组中的特定元素可以通过索引访问,第一个索引值为 0。

声明数组

type arrayName [ arraySize ];

这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。

初始化数组

在 C 中,您可以逐个初始化数组:

balance[4] = 50.0;

也可以使用一个初始化语句:

double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。

如果您省略掉了数组的大小,数组的大小则为初始化时元素的个数。

访问数组元素

数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。

下面的实例使用了上述的三个概念,即,声明数组、数组赋值、访问数组:

#include <stdio.h>
 
int main ()
{
   int n[ 10 ]; /* n 是一个包含 10 个整数的数组 */
   int i,j;
 
   /* 初始化数组元素 */         
   for ( i = 0; i < 10; i++ )
   {
      n[ i ] = i + 100; /* 设置元素 i 为 i + 100 */
   }
   
   /* 输出数组中每个元素的值 */
   for (j = 0; j < 10; j++ )
   {
      printf("Element[%d] = %d\n", j, n[j] );
   }
 
   return 0;
}

多维数组

C 支持多维数组。多维数组最简单的形式是二维数组。

传递数组给函数

您可以通过指定不带索引的数组名称来给函数传递一个指向数组的指针。

从函数返回数组

C 允许从函数返回数组。

指向数组的指针

您可以通过指定不带索引的数组名称来生成一个指向数组中第一个元素的指针。

解说

计算数组元素个数

在我们没有明确数组的元素个数时,在程序中想知道数组单元个数可以使用 sizeof(a)/sizeof(a[0]), sizeof(a) 是得到数组 a 的大小,sizeof(a[0]) 是得到数组 a 中单个元素的大小:

#include<stdio.h>

int main(int argc,char *grgv[])
{
    int a[]={1,2,3,4,5};
    int b;
    b=sizeof(a)/sizeof(a[0]);
    printf("数组元素个数为:%d",b);
    return 0; 
}

C enum(枚举)

枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。

枚举语法定义格式为:

enum 枚举名 {枚举元素1,枚举元素2,……};

比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:

#define MON  1
#define TUE  2
#define WED  3
#define THU  4
#define FRI  5
#define SAT  6
#define SUN  7

这个看起来代码量就比较多,接下来我们看看使用枚举的方式:

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

注意:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。 我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

枚举变量的定义

1、先定义枚举类型,再定义枚举变量

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

enum DAY day;

2、定义枚举类型的同时定义枚举变量

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

3、省略枚举名称,直接定义枚举变量

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

实例:

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
 
int main()
{
    enum DAY day;
    day = WED;
    printf("%d",day);
    return 0;
}

以上实例输出结果为:3

C 指针

每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。

什么是指针?

指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样, 您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var-name;

type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。

所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

如何使用指针?

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。 这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。

#include <stdio.h>
 
int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */
 
   ip = &var;  /* 在指针变量中存储 var 的地址 */
 
   printf("var 变量的地址: %p\n", &var  );
 
   /* 在指针变量中存储的地址 */
   printf("ip 变量存储的地址: %p\n", ip );
 
   /* 使用指针访问值 */
   printf("*ip 变量的值: %d\n", *ip );
 
   return 0;
}

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

var 变量的地址: 0x7ffeeef168d8
ip 变量存储的地址: 0x7ffeeef168d8
*ip 变量的值: 20

C 中的 NULL 指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。

NULL 指针是一个定义在标准库中的值为零的常量。

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
 
   printf("ptr 的地址是 %p\n", ptr  );
 
   return 0;
}

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

ptr 的地址是 0x0

在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。 然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。 但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

指针的算术运算

可以对指针进行四种算术运算:++、--、+、-

指针数组

可以定义用来存储指针的数组。

如:

int *ptr[3];

用一个指向字符的指针数组来存储一个字符串列表,如下:

#include <stdio.h>
 
const int MAX = 4;
 
int main ()
{
   const char *names[] = {
       "Zara Ali",
       "Hina Ali",
       "Nuha Ali",
       "Sara Ali",
   };
   int i = 0;
 
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of names[%d] = %s\n", i, names[i]);
   }
   return 0;
}

printf("%s", a); 中输出字符串%s,后面变量a就应该是一个指针(地址)。

指向指针的指针

C 允许指向指针的指针。

传递指针给函数

通过引用或地址传递参数,使传递的参数在调用函数中被改变。

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

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

#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;
}

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

#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;
}

这里存在一个问题,getAverage(int *arr, int size) 中 arr 可以是一个整数数组的数组名称, 也可以是一个整数变量地址,当对整数变量地址进行数组索引取值时返回异常数据,如:

#include <stdio.h>
 
double getAverage(int *arr, int size);
 
int main ()
{
    int a = 10;
    
    getAverage(&a, 3) ;
   
    return 0;
}

double getAverage(int *arr, int size)
{
    printf("%d\n", arr[0]);
    printf("%d\n", arr[1]);
    printf("%d\n", arr[2]);
    return 0;
}

输出:

10
4198832
0

从函数返回指针

C 允许函数返回指针到局部变量、静态变量和动态内存分配。

让我们来看下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们,具体如下:

#include <stdio.h>
#include <time.h>
#include <stdlib.h> 
 
/* 要生成和返回随机数的函数 */
int * getRandom( )
{
   static int  r[10];
   int i;
 
   /* 设置种子 */
   srand( (unsigned)time( NULL ) );
   for ( i = 0; i < 10; ++i)
   {
      r[i] = rand();
      printf("%d\n", r[i] );
   }
 
   return r;
}
 
/* 要调用上面定义函数的主函数 */
int main ()
{
   /* 一个指向整数的指针 */
   int *p;
   int i;
 
   p = getRandom();
   for ( i = 0; i < 10; i++ )
   {
       printf("*(p + [%d]) : %d\n", i, *(p + i) );
   }
 
   return 0;
}

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

1523198053
1187214107
1108300978
430494959
1421301276
930971084
123250484
106932140
1604461820
149169022
*(p + [0]) : 1523198053
*(p + [1]) : 1187214107
*(p + [2]) : 1108300978
*(p + [3]) : 430494959
*(p + [4]) : 1421301276
*(p + [5]) : 930971084
*(p + [6]) : 123250484
*(p + [7]) : 106932140
*(p + [8]) : 1604461820
*(p + [9]) : 149169022

解说

解说1

int 变量存的是 int 型的值,char 变量存的是 char 型的值,而指针,它是一种特殊的变量,存的是内存地址, 按照这个模板可以把它理解为:“内存地址变量” 存的是 “内存地址”,等价于:“指针变量” 存的是 “内存地址”

操作系统进行资源调度时,会根据这些变量存的地址去请求和使用那个地址代表的内存区域, 这就仿佛像是这个变量存的地址指向了某片内存,人们用 “指针” 来统称所谓的 “内存地址变量”

因此,任何跟指针有关的概念,都可以联系内存地址加以理解,二者必然有联系,数组与指针,函数与指针,都是如此。

内存是线性的,内存以地址空间的形式呈现给我们看的,所以可以说所谓的地址空间也是线性的,指针存放的是内存地址, 所以你可以对地址做 ++,或者 -- 这样的运算。

两个指针不赋 NULL,是坏习惯:

初始化指针不赋 NULL,因为这样的指针会指向一片未知的区域,这样的指针不是空指针,但指向一片访问受限制的内存区域, 你无法使用它,这样的情况下的指针,业界给了它一个形象的名字:“野指针”,而且难以调试, 在许多编译器单步 debug 会出现奇怪的错误,但经常看见的 “Segmentation Fault” 这样的错误,实测当代码多的时候, 这是一个非常蛋疼的错误,野指针就是成因之一,所以看到这样的错误,首先是想想,是否有某些指针没有初始化引起的;

free() 后指针不赋 NULL,为指针分配内存后,指针便可以指向一片合法可使用的内存,但使用 free() 释放那片内存时, 指针依旧存放着那片内存的地址,也就是依旧指向那片内存,但这片内存已经释放,不可访问,这时若不小心使用了这个指针, 便会内存错误,又是会有奇怪的 bug ,代码几百行多点就会难以调试,业界给这样的指针也有个统称:“悬空指针”, 为了避免这种蛋疼的情况出现,一定要释放内存后,给指向这片内存的指针,都赋值为 NULL,从中也可以看出, free() 这个函数释放内存时跟指向这片内存的指针并没有什么关系,不会连着把指针一起搞定掉的! 珍爱生命,远离 “野指针” 与 “悬空指针” !

多级指针,指向指针的指针,有时人们也管它叫多维指针。既然指针变量是一个变量,指针变量能存变量的内存的地址。

像 int * 存 int 型变量的地址,char * 存 char 型的地址,那指针理所当然可以存指针变量的地址啊。

例如,int ** 存 int * 的地址,int *** 存 int ** 的地址。

这就是一个二级指针存一级指针的地址,三级指针存二级指针的地址,人们把这样的过程叫指向指针的指针, 但其实也就是一个上一级的指针存了下一级的指针的地址而已。

因此,像上面说的,你存了它的地址,你就是指向它,所以:

二级指针存一级指针的地址,那么可以说二级指针指向一级指针
三级指针存二级指针的地址,那么可以说三级指针指向二级指针
多级指针用处多多, 这里暂不举例详细说明。 

个人认为指针可以说是 C 的最伟大的特性,通过这样的一个模型可以形象地管理部分内存!

C 函数指针与回调函数

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

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

函数指针变量的声明:

typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
#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;
}

回调函数

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

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

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

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

#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];
    /* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/
    populate_array(myarray, 10, getNextRandomValue);
    for(int i = 0; i < 10; i++) {
        printf("%d ", myarray[i]);
    }
    printf("\n");
    return 0;
}

size_t 类型在C语言标准库函数原型使用的很多,数值范围一般是要大于int和unsigned.

但凡不涉及负值范围的表示size取值的,都可以用size_t;比如array[size_t]

size_t 在stddef.h头文件中定义。

在其他常见的宏定义以及函数中常用到有:

1,sizeof运算符返回的结果是size_t类型;

2,void *malloc(size_t size)

在vcruntime.h 看到如下定义:

// Definitions of common types
#ifdef _WIN64
    typedef unsigned __int64 size_t;
    typedef __int64          ptrdiff_t;
    typedef __int64          intptr_t;
#else
    typedef unsigned int     size_t;
    typedef int              ptrdiff_t;
    typedef int              intptr_t;
#endif

C 字符串

在 C 语言中,字符串实际上是使用 null 字符 \0 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。

char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
char site[] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
char site[] = "RUNOOB";
char site[] = {"RUNOOB"};
char *site = "RUNOOB";  // 字符指针:(注意指针不能直接赋给数组)

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

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

strcmp: string compare

strcat: string catenate

strcpy: string copy

strlen: string length

strlwr: string lowercase

strupr: string upercase

解说

strlen 与 sizeof的区别

strlen 是函数,sizeof 是运算操作符,二者得到的结果类型为 size_t,即 unsigned int 类型。 sizeof 计算的是变量的大小,不受字符 \0 影响;

而 strlen 计算的是字符串的长度,以 \0 作为长度判定依据。

C 结构体

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

定义结构

struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;

tag 是结构体标签。

member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。

variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。

在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。

//此声明声明了拥有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;

结构体变量的初始化

和其它类型变量一样,对结构体变量可以在定义时指定初始值。

struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};

访问结构成员

了访问结构的成员,我们使用成员访问运算符(.)。

结构作为函数参数

把结构作为函数参数,传参方式与其他类型的变量或指针类似。

#include <stdio.h>
#include <string.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
/* 函数声明 */
void printBook( struct Books book );
int main( )
{
   struct Books Book1;        /* 声明 Book1,类型为 Books */
   struct Books Book2;        /* 声明 Book2,类型为 Books */
 
   /* Book1 详述 */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;
 
   /* Book2 详述 */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* 输出 Book1 信息 */
   printBook( Book1 );
 
   /* 输出 Book2 信息 */
   printBook( Book2 );
 
   return 0;
}
void printBook( struct Books book )
{
   printf( "Book title : %s\n", book.title);
   printf( "Book author : %s\n", book.author);
   printf( "Book subject : %s\n", book.subject);
   printf( "Book book_id : %d\n", book.book_id);
}

指向结构的指针

定义指向结构的指针,方式与定义指向其他类型变量的指针相似

struct Books *struct_pointer;
struct_pointer = &Book1;
struct_pointer->title;

解说

结构体内存大小对齐原则

结构体变量的首地址能够被其最宽基本类型成员的大小所整除。

结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。 即结构体成员的末地址减去结构体首地址(第一个结构体成员的首地址)得到的偏移量都要是对应成员大小的整数倍。

结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在成员末尾加上填充字节。

C 共用体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。 共用体提供了一种使用相同的内存位置的有效方式。

定义共用体

union [union tag]
{
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];

union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。 在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。

共用体占用的内存应足够存储共用体中最大的成员。

访问共用体成员

为了访问共用体的成员,我们使用成员访问运算符(.)。

解说

结构体与共用体

结构体变量所占内存长度是其中最大字段大小的整数倍。

共用体变量所占的内存长度等于最长的成员变量的长度。

对齐原则

【原则1】数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方, 以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

【原则2】结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐, 对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

【原则3】结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

union Data{
    int i;
    float f;
    char str[9];
    double d;
}data;

按照原则 2,VS 中默认的是 #pragma park(8),char[9] 长度为 9。

8<9; 按照 8 的倍数且 >9,则取为 16;

C 位域

位域声明

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。 为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为”位域”或”位段”。

所谓”位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域的定义和位域变量的说明

位域定义与结构定义相仿,其形式为:

struct 位域结构名 
{
    位域列表
};

其中位域列表的形式为:

type [member_name] : width ;
元素    描述
type    只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。
member_name    位域的名称。
width    位域中位的数量。宽度必须小于或等于指定类型的位宽度。

带有预定义宽度的变量被称为位域。位域可以存储多于 1 位的数,例如,需要一个变量来存储从 0 到 7 的值,您可以定义一个宽度为 3 位的位域,如下:

struct
{
  unsigned int age : 3;
} Age;
struct packed_struct {
  unsigned int f1:1;
  unsigned int f2:1;
  unsigned int f3:1;
  unsigned int f4:1;
  unsigned int type:4;
  unsigned int my_int:9;
} pack;

在这里,packed_struct 包含了 6 个成员:四个 1 位的标识符 f1..f4、一个 4 位的 type 和一个 9 位的 my_int。

对于位域的定义尚有以下几点说明:

  • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
  • 位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度。
  • 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。
struct bs{
    unsigned a:4;
    unsigned  :4;    /* 空域,该 4 位不能使用 */
    unsigned b:4;    /* 从下一单元开始存放 */
    unsigned c:4
}

位域的使用

位域的使用和结构成员的使用相同,其一般形式为:

位域变量名.位域名
位域变量名->位域名

位域允许用各种格式输出。

解说

结构体内存分配原则

原则一:结构体中元素按照定义顺序存放到内存中,但并不是紧密排列。从结构体存储的首地址开始 ,每一个元素存入内存中时, 它都会认为内存是以自己的宽度来划分空间的,因此元素存放的位置一定会在自己大小的整数倍上开始。

原则二: 在原则一的基础上,检查计算出的存储单元是否为所有元素中最宽的元素长度的整数倍。若是,则结束;否则,将其补齐为它的整数倍。

C typedef

C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。

typedef unsigned char BYTE;
BYTE  b1, b2;
#include <stdio.h>
#include <string.h>
 
typedef struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} Book;
 
int main( )
{
   Book book;
 
   strcpy( book.title, "C 教程");
   strcpy( book.author, "Runoob"); 
   strcpy( book.subject, "编程语言");
   book.book_id = 12345;
 
   printf( "书标题 : %s\n", book.title);
   printf( "书作者 : %s\n", book.author);
   printf( "书类目 : %s\n", book.subject);
   printf( "书 ID : %d\n", book.book_id);
 
   return 0;
}

typedef vs #define

#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

  • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

(1)#define可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。

(2) 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。

C 输入 & 输出

当我们提到输入时,这意味着要向程序填充一些数据。输入可以是以文件的形式或从命令行中进行。

当我们提到输出时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据。

标准文件

C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。

标准文件    文件指针    设备
标准输入    stdin    键盘
标准输出    stdout    屏幕
标准错误    stderr    您的屏幕
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。

int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。

char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。

int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

int scanf(const char *format, ...) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

int printf(const char *format, ...) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

C 文件读写

一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。

打开文件

使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象, 类型 FILE 包含了所有用来控制流的必要的信息:

FILE *fopen( const char * filename, const char * mode );

filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:

模式    描述
r    打开一个已有的文本文件,允许读取文件。
w    打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a    打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+    打开一个文本文件,允许读写文件。
w+    打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+    打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

关闭文件

为了关闭文件,请使用 fclose( ) 函数

int fclose( FILE *fp );

如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。 这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

写入文件

int fputc( int c, FILE *fp );  // 函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中

int fputs( const char *s, FILE *fp );  // 函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。

int fprintf(FILE *fp,const char *format, ...)   // 函数把一个字符串写入到文件中

示例:

#include <stdio.h>
 
int main()
{
   FILE *fp = NULL;
 
   fp = fopen("/tmp/test.txt", "w+");
   fprintf(fp, "This is testing for fprintf...\n");
   fputs("This is testing for fputs...\n", fp);
   fclose(fp);
}

读取文件

int fgetc( FILE * fp );   // fgetc() 函数从 fp 所指向的输入文件中读取一个字符
 
char *fgets( char *buf, int n, FILE *fp );  // 函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符

int fscanf(FILE *fp, const char *format, ...)   // 函数来从文件中读取字符串

示例:

#include <stdio.h>
 
int main()
{
   FILE *fp = NULL;
   char buff[255];
 
   fp = fopen("/tmp/test.txt", "r");
   fscanf(fp, "%s", buff);
   printf("1: %s\n", buff );
 
   fgets(buff, 255, (FILE*)fp);
   printf("2: %s\n", buff );
   
   fgets(buff, 255, (FILE*)fp);
   printf("3: %s\n", buff );
   fclose(fp);
 
}

输出:

1: This
2: is testing for fprintf...
3: This is testing for fputs...

首先,fscanf() 方法只读取了 This,因为它在后边遇到了一个空格。 其次,调用 fgets() 读取剩余的部分,直到行尾。最后,调用 fgets() 完整地读取第二行。

二进制 I/O 函数

下面两个函数用于二进制输入和输出:

size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
              
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

解读

解读1

fseek 可以移动文件指针到指定位置读,或插入写:

int fseek(FILE *stream, long offset, int whence); 

fseek 设置当前读写点到 offset 处, whence 可以是 SEEK_SET,SEEK_CUR,SEEK_END 这些值决定是从文件头、当前点和文件尾计算偏移量 offset。

你可以定义一个文件指针 FILE *fp,当你打开一个文件时,文件指针指向开头,你要指到多少个字节,只要控制偏移量就好, 例如, 相对当前位置往后移动一个字节:fseek(fp,1,SEEK_CUR); 中间的值就是偏移量。 如果你要往前移动一个字节,直接改为负值就可以:fseek(fp,-1,SEEK_CUR)

解读2

在新版的 VS 编译环境中提示 fopen 不安全,推荐使用 fopen_s 代替。

fopen_s 和 fopen 的区别在于:

errno_t fopen_s( FILE** pFile, const char *filename, const char *mode );

FILE *fopen(const char *filename, const char *mode)

fopen_s 有三个参数,第一个参数是二级指针,用于存放文件流指针地址,其他的同 fopen 一致。

也就是说,用 fopen 需要这么写:

FILE* fp=NULL;
fp=fopen("\\123.txt","w+");

而如果用 fopen_s:

FILE* fp=NULL;
fopen_s(&fp,"\\123.txt","w+");

解读3

对 fopen()函数补充说明几点:

该函数可能执行失败,返回值是NULL,安全起见必须对返回值进行合法性判断;

该函数有多种模式,其中r+和w+看似一样,都是读写其实还是有几点区别的;

  1. 模式r+找不到文件不会自动新建,而w+会;
  2. 模式r+打开文件后,不会清除文件原数据,若直接开始写入,只会从起始位置开始进行覆盖,而w+会直接清零后,再开始读写;

模式的合法性说明:不能用大写,只能是小写,且rb+和r+b都是合法的,但br+和+rb等都是非法的,w和a也是一样的处理;

模式w的自动新建文件是有条件的,只有对应的路径存在(即文件所在的文件夹存在),文件不存在才会新建,否则是不会新建的,返回NULL

解读4

C 语言中 printf 输出 double 和 float 都可以用 %f 占位符 可以混用,而 double 可以额外用 %lf。

而 scanf 输入情况下 double 必须用 %lf,float 必须用 %f 不能混用。

C 预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已, 它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性, 预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

指令    描述
#define    定义宏
#include    包含一个源代码文件
#undef    取消已定义的宏
#ifdef    如果宏已经定义,则返回真
#ifndef    如果宏没有定义,则返回真
#if    如果给定条件为真,则编译下面代码
#else    #if 的替代方案
#elif    如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif    结束一个 #if……#else 条件编译块
#error    当遇到标准错误时,输出错误消息
#pragma    使用标准化方法,向编译器发布特殊的命令到编译器中

预定义宏

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

宏    描述
__DATE__    当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
__TIME__    当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
__FILE__    这会包含当前文件名,一个字符串常量。
__LINE__    这会包含当前行号,一个十进制常量。
__STDC__    当编译器以 ANSI 标准编译时,则定义为 1。

示例:

#include <stdio.h>

main()
{
   printf("File :%s\n", __FILE__ );
   printf("Date :%s\n", __DATE__ );
   printf("Time :%s\n", __TIME__ );
   printf("Line :%d\n", __LINE__ );
   printf("ANSI :%d\n", __STDC__ );

}

输出:

File :test.c
Date :Jun 2 2012
Time :03:36:24
Line :8
ANSI :1

预处理器运算符

宏延续运算符(\

字符串常量化运算符(#

标记粘贴运算符(##

defined() 运算符

示例:

#include <stdio.h>

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

int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

输出:

token34 = 40

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

参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。

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

#include <stdio.h>

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

#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;
}

使用#define含参时,参数括号很重要。

C 头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。 有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

#include <file>     // 引用系统头文件,引用的是编译器的类库路径里面的头文件。

#include "file"     // 引用用户头文件,引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件。

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出, 被引用文件生成的输出以及 #include 指令之后的文本输出。

C 强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。

(type_name) expression

类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。

整数提升

整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。

常用的算术转换

常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:

常用的算术转换不适用于赋值运算符、逻辑运算符 &&||

实例:

#include <stdio.h>
 
int main()
{
   int  i = 17;
   char c = 'c'; /* ascii 值是 99 */
   float sum;
 
   sum = i + c;
   printf("Value of sum : %f\n", sum );
 
}

输出:

Value of sum : 116.000000

在这里,c 首先被转换为整数,但是由于最后的值是 float 型的,所以会应用常用的算术转换, 编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

C 错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。 在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量, 表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。 开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

errno、perror() 和 strerror()

  • errno 全局变量,具体错误代码。
  • perror() 函数,显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
  • strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
  • stderr 文件流,用来输出错误

让我们来模拟一种错误情况,尝试打开一个不存在的文件:

#include <stdio.h>
#include <errno.h>
#include <string.h>
 
extern int errno ;
 
int main ()
{
   FILE * pf;
   int errnum;
   pf = fopen ("unexist.txt", "rb");
   if (pf == NULL) {
      errnum = errno;
      fprintf(stderr, "错误号: %d\n", errno);
      perror("通过 perror 输出错误");
      fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
   } else {
      fclose (pf);
   }
   return 0;
}

输出:

错误号: 2
通过 perror 输出错误: No such file or directory
打开文件错误: No such file or directory

程序退出状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。

如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。

被零除的错误:

#include <stdio.h>
#include <stdlib.h>
 
main()
{
   int dividend = 20;
   int divisor = 5;
   int quotient;
 
   if (divisor == 0) {
      fprintf(stderr, "除数为 0 退出运行...\n");
      exit(EXIT_FAILURE);
   }
   
   quotient = dividend / divisor;
   fprintf(stdout, "quotient 变量的值为: %d\n", quotient );
 
   exit(EXIT_SUCCESS);
}

C 递归

递归指的是在函数的定义中使用函数自身的方法。

语法格式如下:


void recursion()
{
   statements;
   ... ... ...
   recursion(); /* 函数调用自身 */
   ... ... ...
}
 
int main()
{
   recursion();
}

流程图:

C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。

数的阶乘:

#include <stdio.h>
 
double factorial(unsigned int i)
{
   if (i <= 1) {
      return 1;
   }
   return i * factorial(i - 1);
}
int  main()
{
    int i = 15;
    printf("%d 的阶乘为 %f\n", i, factorial(i));
    return 0;
}

斐波那契数列:

#include <stdio.h>
 
int fibonaci(int i)
{
   if (i == 0) {
      return 0;
   }
   if (i == 1) {
      return 1;
   }
   return fibonaci(i-1) + fibonaci(i-2);
}
 
int  main()
{
    int i;
    for (i = 0; i < 10; i++)
    {
       printf("%d\t\n", fibonaci(i));
    }
    return 0;
}

输出:

0    
1    
1    
2    
3    
5    
8    
13    
21    
34

解说

解说1

递归是一个简洁的概念,同时也是一种很有用的手段。但是,使用递归是要付出代价的。与直接的语句(如while循环)相比, 递归函数会耗费更多的运行时间,并且要占用大量的栈空间。递归函数每次调用自身时,都需要把它的状态存到栈中, 以便在它调用完自身后,程序可以返回到它原来的状态。

解说2

采用递归方法来解决问题,必须符合以下三个条件:

1、可以把要解决的问题转化为一个新问题,而这个新的问题的解决方法仍与原来的解决方法相同,只是所处理的对象有规律地递增或递减。

说明:解决问题的方法相同,调用函数的参数每次不同(有规律的递增或递减),如果没有规律也就不能适用递归调用。

2、可以应用这个转化过程使问题得到解决。

说明:使用其他的办法比较麻烦或很难解决,而使用递归的方法可以很好地解决问题。

3、必定要有一个明确的结束递归的条件。

说明:一定要能够在适当的地方结束递归调用。不然可能导致系统崩溃。

解说3

  1. 电脑空间大致分Heap(堆)和Stack(栈)两种。 栈是用于函数的空间。 电脑调用一个函数,就会使用一层栈; 相反,电脑中一个函数结束(return),就会释放这一层栈,连同在这层栈(这个函数)中定义的所有东西。 不在栈中的,应该就在堆中。(这就是定义全区变量与局部变量的用处) 如果调用太多层栈(太多个函数),电脑就会暴空间! 所以说,调用递归函数,就会一层一层地压栈,电脑就会暴空间!
  2. 递归,就是递(一层一层地调用),归(一层一层地返回),这样会费很多时间!容易超时! 但是,我并不是说不用递归,而是说能用递推算法的,最好不用递归算法。
  3. 递归,是一种算法,特点:函数调用本身。
  4. 我们在(1.)说过了,在此说一下:数据结构——栈,可以用递归来实现。
  5. 递归写出来的C程序一般都很简洁。
  6. 有些算法,如搜索与回溯算法,广度优先搜索算法,分治(二分),都用到递归。

C 可变参数

有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。 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 变量的内存。

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


#include <stdio.h>
#include <stdarg.h>
 
double average(int num,...)
{
    /* 创建 valist 类型变量 */
    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 of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000

解说

解说1

C 函数要在程序中用到以下这些宏:

void va_start( va_list arg_ptr, prev_param ); 
type va_arg( va_list arg_ptr, type ); 
void va_end( va_list arg_ptr );
  • va_list: 用来保存宏va_start、va_arg和va_end所需信息的一种类型。为了访问变长参数列表中的参数, 必须声明 va_list 类型的一个对象,定义: typedef char * va_list;
  • va_start: 访问变长参数列表中的参数之前使用的宏,它初始化用 va_list 声明的对象,初始化结果供宏 va_arg 和 va_end 使用;
  • va_arg: 展开成一个表达式的宏,该表达式具有变长参数列表中下一个参数的值和类型。每次调用 va_arg 都会修改用 va_list 声明的对象, 从而使该对象指向参数列表中的下一个参数;
  • va_end: 该宏使程序能够从变长参数列表用宏 va_start 引用的函数中正常返回。
  • va 在这里是 variable-argument(可变参数) 的意思。

这些宏定义在 stdarg.h 中,所以用到可变参数的程序应该包含这个头文件。

下面我们写一个简单的可变参数的函数:

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
 
int demo(char *msg, ... )  
{  
    va_list argp;                    /* 定义保存函数参数的结构 */  
    int argno = 0;                   /* 纪录参数个数 */  
    char *para;                      /* 存放取出的字符串参数 */                                      
    va_start( argp, msg );           /* argp指向传入的第一个可选参数,  msg是最后一个确定的参数 */  
    
    while (1) 
    {  
        para = va_arg( argp, char *);               /* 取出当前的参数,类型为char *. */  
        if ( strcmp( para, "/0") == 0 )  {          /* 采用空串指示参数输入结束 */                                
            break;  
        }
        printf("Parameter #%d is: %s\n", argno, para);  
        argno++;  
    }  
    va_end( argp );                                   /* 将argp置为NULL */  
    return 0;  
}

int main( void )  
{  
    demo("DEMO", "This", "is", "a", "demo!" ,"333333", "/0");  
} 

输出:

Parameter #0 is: This
Parameter #1 is: is
Parameter #2 is: a
Parameter #3 is: demo!
Parameter #4 is: 333333

C 内存管理

C 语言为内存的分配和管理提供了几个函数。这些函数可以在 头文件中找到:

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 * 类型可以通过类型转换强制转换为任何其它类型的指针。

看一个 动态分配内存 及 重新调整内存的大小 和 释放内存 的例子:

#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 = (char *) 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);
}

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

Name = Zara Ali
Description: Zara ali a DPS student.She is in class 10th

可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。

上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可:

calloc(30, sizeof(char));

当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议在不需要内存时,都应该调用函数 free() 来释放内存。 或者,可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。

解说

解说1

对于 void 指针,GNU 认为 void *char * 一样,所以以下写法是正确的:

description = malloc( 200 * sizeof(char) );

但按照 ANSI(American National Standards Institute) 标准,需要对 void 指针进行强制转换,如下:

description = (char *)malloc( 200 * sizeof(char) );

同时,按照 ANSI(American National Standards Institute) 标准,不能对 void 指针进行算法操作:

void * pvoid;
pvoid++; //ANSI:错误
pvoid += 1; //ANSI:错误
// ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。

int *pint;
pint++; //ANSI:正确

解说2

动态可变长的结构体:

typedef struct
{
  int id;
  char name[0];
} stu_t;

定义该结构体,只占用4字节的内存,name不占用内存。

stu_t *s = NULL;    // 定义一个结构体指针
s = malloc(sizeof(*s) + 100);  // sizeof(*s)获取的是4,但加上了100,4字节给id成员使用,100字节是属于name成员的
s->id = 1010;
strcpy(s->name,"hello");

注意:一个结构体中只能有一个可变长的成员,并且该成员必须是最后一个成员。

void* 详解及应用

void 在英文中作为名词的解释为 “空虚、空间、空隙”,而在 C 语言中,void 被翻译为”无类型”,相应的void * 为”无类型指针”。

void 似乎只有”注释”和限制程序的作用,当然,这里的”注释”不是为我们人提供注释,而是为编译器提供一种所谓的注释。

void 的作用

  1. 对函数返回的限定,这种情况我们比较常见。
  2. 对函数参数的限定,这种情况也是比较常见的。

一般我们常见的就是这两种情况:

  • 当函数不需要返回值值时,必须使用void限定,这就是我们所说的第一种情况。例如:void func(int a,char *b)
  • 当函数不允许接受参数时,必须使用void限定,这就是我们所说的第二种情况。例如:int func(void)

void 指针的使用规则

1.void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。例如:

int *a;
void *p;
p=a;

如果要将 void 指针 p 赋给其他类型的指针,则需要强制类型转换,就本例而言:a=(int *)p。 在内存的分配中我们可以见到 void 指针使用:内存分配函数 malloc 函数返回的指针就是 void * 型, 用户在使用这个指针的时候,要进行强制类型转换,也就是显式说明该指针指向的内存中是存放的什么类型的数据, (int *)malloc(1024) 表示强制规定 malloc 返回的 void * 指针指向的内存中存放的是一个的 int 型数据。

2.在 ANSI C 标准中,不允许对 void 指针进行一些算术运算如 p++p+=1 等,因为既然 void 是无类型, 那么每次算术运算我们就不知道该操作几个字节,例如 char 型操作 sizeof(char) 字节,而 int 则要操作 sizeof(int) 字节。 而在 GNU 中则允许,因为在默认情况下,GNU 认为 void *char * 一样,既然是确定的, 当然可以进行一些算术操作,在这里sizeof(*p)==sizeof(char)

众所周知,如果指针 p1 和 p2 的类型相同,那么我们可以直接在 p1 和 p2 间互相赋值; 如果 p1 和 p2 指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。

float *p1;
int *p2;
p1 = p2;
//其中p1 = p2语句会编译出错,
//提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为:
p1 = (float *)p2;

void * 则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换。

void *p1;
int *p2;
p1 = p2;

但这并不意味着,void * 也可以无需强制类型转换地赋给其它类型的指针。 因为”无类型”可以包容”有类型”,而”有类型”则不能包容”无类型”。

小心使用 void 指针类型:按照 ANSI(American National Standards Institute) 标准,不能对 void 指针进行算法操作。 但是 GNU 则不这么认定,它指定 void * 的算法操作与 char * 一致。

在实际的程序设计中,为迎合 ANSI 标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:

void * pvoid;
((char *)pvoid)++;
(char *)pvoid += 1

GNU 和 ANSI 还有一些区别,总体而言,GNU 较 ANSI 更”开放”,提供了对更多语法的支持。 但是我们在真实设计时,还是应该尽可能地迎合 ANSI 标准。 如果函数的参数可以是任意类型指针,那么应声明其参数为void *。 这样函数就可以接受任意类型的指针。如典型的如内存操作函数 memcpy 和 memset 的函数原型分别为:

void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );

这样,任何类型的指针都可以传入 memcpy 和 memset 中,这也真实地体现了内存操作函数的意义, 因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。

C语言实现泛型编程

泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。 在 C 语言中,可以通过一些手段实现这样的泛型编程。这里介绍一种方法——通过无类型指针 void *

实现交换两个元素内容的函数 swap:

void swap(void *vp1,void *vp2,int size){  
      char buffer[size];  //注意此处gcc编译器是允许这样声明的
      memcpy(buffer,vp1,size);  
      memcpy(vp1,vp2,size);  
      memcpy(vp2,buffer,size);  
}  

在调用这个函数时,可以像如下这样调用(同样适用于其它类型的 x、y):

int x = 27,y = 2;  
swap(&x,&y,sizeof(int));

C 命令行参数

执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数。

命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数, argv[] 是一个指针数组,指向传递给程序的每个参数。

argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针,*argv[n] 是最后一个参数。

多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号 "" 或单引号 '' 内部。

向程序传递一个放置在双引号内部的命令行参数:

#include <stdio.h>

int main( int argc, char *argv[] )  
{
   printf("Program name %s\n", argv[0]);
 
   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");
   }
}

编译并执行上面的代码,它会产生下列结果:

$./a.out "testing1 testing2"

Progranm name ./a.out
The argument supplied is testing1 testing2






参考资料

C语言RUNOOB教程 https://www.runoob.com/cprogramming/c-tutorial.html

C 语言中 void* 详解及应用 https://www.runoob.com/w3cnote/c-void-intro.html

C语言实现泛型编程 https://www.runoob.com/w3cnote/c-general-function.html


返回