基础

数据类型
C 中的类型可分为以下几种:
序号 | 类型与描述 |
---|---|
1 | 基本类型: 它们是算术类型,包括两种类型:整数类型和浮点类型。 |
2 | 枚举类型: 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。 |
3 | void 类型: 类型说明符 void 表明没有可用的值。 |
4 | 派生类型: 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。 |
void 类型指定没有可用的值。它通常用于以下三种情况下:
类型 | 描述 |
---|---|
函数返回为空 | C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status); |
函数参数为空 | C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void); |
指针指向 void | 类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。 |
类型 | 存储大小 | 值范围 |
---|---|---|
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 |
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
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 位小数 |
数据类型别名:
typedef unsigned char uint8_t; //无符号8位数 typedef signed char int8_t; //有符号8位数 typedef unsigned int uint16_t; //无符号16位数 typedef signed int int16_t; //有符号16位数 typedef unsigned long uint32_t; //无符号32位数 typedef signed long int32_t; //有符号32位数 typedef float float32; //单精度浮点数 typedef double float64; //双精度浮点数 //size_t也是一种数据类型,近似于无符号整型,但容量范围一般大于 int 和 unsigned。 |
数据类型转换:
C 语言中如果一个表达式中含有不同类型的常量和变量,在计算时,会将它们自动转换为同一种类型;
在 C 语言中也可以对数据类型进行强制转换;
强制类型转换形式: (类型说明符)(表达式)
关键字
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
const | 声明只读变量 |
default | 开关语句中的”其它”分支 |
double | 声明双精度浮点型变量或函数返回值类型 |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
C99 新增关键字
_Bool |
_Complex |
_Imaginary |
inline |
restrict |
---|---|---|---|---|
C11 新增关键字
_Alignas |
_Alignof |
_Atomic |
_Generic |
_Noreturn |
---|---|---|---|---|
_Static_assert |
_Thread_local |
typedef
C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。
typedef unsigned char BYTE;
|
您也可以使用 typedef 来为用户自定义的数据类型取一个新的名字。例如,您可以对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量,如下:
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( "书标题 : %sn", book.title); printf( "书作者 : %sn", book.author); printf( "书类目 : %sn", book.subject); printf( "书 ID : %dn", book.book_id); return 0; } |
数组别名
typedef int A[6];
|
表示用 A 代替 int [6]。
即:A a; 等于 int a[6];
简化复杂声明
typedef 还有一个作用,就是为复杂的声明定义一个新的简单的别名。用在回调函数中特别好用:
1).原声明:int *(*a[5])(int, char*);
在这里,变量名为 a,直接用一个新别名 pFun 替换 a 就可以了:
typedef int *(*pFun)(int, char*);
|
于是,原声明的最简化版:
pFun a[5];
|
2).原声明:void (*b[10]) (void (*)());
这里,变量名为 b,先替换右边部分括号里的,pFunParam 为别名一:
typedef void (*pFunParam)();
|
再替换左边的变量 b,pFunx 为别名二:
typedef void (*pFunx)(pFunParam);
|
于是,原声明的最简化版:
pFunx b[10];
|
其实,可以这样理解:
typedef int *(*pFun)(int, char*);
|
由 typedef 定义的函数 pFun,为一个新的类型,所以这个新的类型可以像 int 一样定义变量,于是,pFun a[5];就定义了 int *(*a[5])(int, char*);
所以我们可以用来定义回调函数,特别好用。
另外,也要注意,typedef 在语法上是一个存储类的关键字(如 auto、extern、mutable、static、register 等一样),虽然它并不真正影响对象的存储特性,如:
typedef static int INT2; // 不可行
|
编译将失败,会提示“指定了一个以上的存储类”。
typedef与define
#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
- typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
- typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
1)#define可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。例如:
#define INTERGE int; unsigned INTERGE n; //没问题 typedef int INTERGE; unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned |
(2) 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
#define PTR_INT int * PTR_INT p1, p2; //p1、p2 类型不相同,宏展开后变为int *p1, p2; typedef int * PTR_INT PTR_INT p1, p2; //p1、p2 类型相同,它们都是指向 int 类型的指针。 |
变量
变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。
变量的声明有两种情况:
- 1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
- 2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。
- 除非有extern关键字,否则都是变量的定义。
extern int i; //声明,不是定义 int i; //声明,也是定义 |
如果需要在一个源文件中引用另外一个源文件中定义的变量,我们只需在引用的文件中将变量加上 extern 关键字的声明即可。
//test.c /*外部变量声明*/ extern int x ; extern int y ; int addtwonum() { return x+y; } |
//main.c /*定义两个全局变量*/ int x=1; int y=2; int addtwonum(); int main(void) { int result; result = addtwonum(); printf("result 为: %dn",result); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
$ gcc addtwonum.c test.c -o main $ ./main result 为: 3 |
常量
常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。
//整数常量 85 /* 十进制 */ 0213 /* 八进制 */ 0x4b /* 十六进制 */ 30 /* 整数 */ 30u /* 无符号整数 */ 30l /* 长整数 */ 30ul /* 无符号长整数 */ |
//浮点常量 /* 当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。 当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。 带符号的指数是用 e 或 E 引入的。 */ 3.14159 /* 合法的 */ 314159E-5L /* 合法的 */ 510E /* 非法的:不完整的指数 */ 210f /* 非法的:没有小数或指数 */ .e55 /* 非法的:缺少整数或分数 */ |
制表符:
转义序列 | 含义 |
---|---|
a | 警报铃声 |
b | 退格键 |
f | 换页符 |
r | 回车 |
t | 水平制表符 |
v | 垂直制表符 |
ooo | 一到三位的八进制数 |
xhh . . . | 一个或多个数字的十六进制数 |
//字符串常量 //可以使用空格做分隔符,把一个很长的字符串常量进行分行。 //下面这三种形式所显示的字符串是相同的。 "hello, dear" "hello, dear" "hello, " "d" "ear" |
定义常量
在 C 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器。
- 使用 const 关键字。
int main() { int area; area = LENGTH * WIDTH; printf("value of area : %d", area); printf("%c", NEWLINE); return 0; } |
int main() { const int LENGTH = 10; const int WIDTH = 5; const char NEWLINE = 'n'; int area; area = LENGTH * WIDTH; printf("value of area : %d", area); printf("%c", NEWLINE); return 0; } |
递归
递归指的是在函数的定义中使用函数自身的方法。
语法格式如下:
void recursion() { statements; ... ... ... recursion(); /* 函数调用自身 */ ... ... ... } int main() { recursion(); } |
C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。
递归函数在解决许多数学问题上起了至关重要的作用,比如计算一个数的阶乘、生成斐波那契数列,等等。
下面的实例使用递归函数计算一个给定的数的阶乘:
double factorial(unsigned int i) { if(i <= 1) { return 1; } return i * factorial(i - 1); } int main() { int i = 15; printf("%d 的阶乘为 %fn", i, factorial(i)); return 0; } |
下面的实例使用递归函数生成一个给定的数的斐波那契数列:
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("%dtn", fibonaci(i)); } return 0; } |
采用递归方法来解决问题,必须符合以下三个条件:
1、可以把要解决的问题转化为一个新问题,而这个新的问题的解决方法仍与原来的解决方法相同,只是所处理的对象有规律地递增或递减。
说明:解决问题的方法相同,调用函数的参数每次不同(有规律的递增或递减),如果没有规律也就不能适用递归调用。
2、可以应用这个转化过程使问题得到解决。
说明:使用其他的办法比较麻烦或很难解决,而使用递归的方法可以很好地解决问题。
3、必定要有一个明确的结束递归的条件。
说明:一定要能够在适当的地方结束递归调用。不然可能导致系统崩溃。
存储类
存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。
下面列出 C 程序中可用的存储类:
- auto
- register
- static
- extern
//auto 存储类是所有局部变量默认的存储类,auto 只能用在函数内部。 { int mount; auto int month; } |
//register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。 //定义 'register' 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。 { register int miles; } |
/* static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。 使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。 static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。 */ /* 函数声明 */ void func1(void); static int count=10; /* 全局变量 - static 是默认的 */ int main() { while (count--) { func1(); } return 0; } void func1(void) { /* 'thingy' 是 'func1' 的局部变量 - 只初始化一次 * 每次调用函数 'func1' 'thingy' 值不会被重置。 */ static int thingy=5; thingy++; printf(" thingy 为 %d , count 为 %dn", thingy, count); } //结果如下 thingy 为 6 , count 为 9 thingy 为 7 , count 为 8 thingy 为 8 , count 为 7 thingy 为 9 , count 为 6 thingy 为 10 , count 为 5 thingy 为 11 , count 为 4 thingy 为 12 , count 为 3 thingy 为 13 , count 为 2 thingy 为 14 , count 为 1 thingy 为 15 , count 为 0 |
/* extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。 当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。 可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。 */ //main.c int count ; extern void write_extern(); int main() { count = 5; write_extern(); } //test.c extern int count; void write_extern(void) { printf("count is %dn", count); } |
运算符
逻辑运算符
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 |
位运算符
位运算符作用于位,并逐位执行操作。
下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与操作,按二进制位进行”与”运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; |
(A & B) 将得到 12,即为 0000 1100 |
| | 按位或运算符,按二进制位进行”或”运算。运算规则:`0 | 0=0; 0 |
^ | 异或运算符,按二进制位进行”异或”运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; |
(A ^ B) 将得到 49,即为 0011 0001 |
~ | 取反运算符,按二进制位进行”取反”运算。运算规则:~1=0; ~0=1; |
(~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
利用异或交换两个数的值:
unsigned int a=60; //0011 1100 unsigned int b=13; //0000 1101 a=a^b; //a=a^b=0011 0001 b=a^b; //b=a^b=0011 1100 相当于b1=(a^b)^b a=a^b; //a=a^b=0000 1101 相当于a1=(a^b)^((a^b)^b) |
利用位与 & 运算,判断一个整数是否是2的整数次幂:
二进制数的位权是以2为底的幂,如果一个整数 m 是 2 的 n 次幂,那么转换为二进制之后只有最高位为 1,其余位置为 0,再观察 m-1 转换为二进制后的形式以及 m&(m-1) 的结果,例如:
2 --> 0000 0010 1 --> 0000 0001 2&1 --> 0000 0010 & 0000 0001 = 0 4 --> 0000 0100 3 --> 0000 0011 4&3 --> 0000 0100 & 0000 0011 = 0 8 --> 0000 1000 7 --> 0000 0111 8&7 --> 0000 1000 & 0000 0111 = 0 |
可以看出所有的 1 完美的错过了,根据位与的特点可知 m&(m-1) 的结果为 0。
如果整数 m 不是 2 的 n 次幂,结果会怎样呢?例如 m=9 时:
9 --> 0000 1001 8 --> 0000 1000 9&8 --> 0000 1001 & 0000 1000 != 0
|
利用这一特点,即可判断一个整数是否是2的整数次幂。
示例:
int func(int num) { return ((num > 0) && ((num & (num - 1)) == 0));//2的n次幂大于0 } |
返回值为 1,则输入的正整数为 2 的整数次幂,返回值为 0 则不是。
对取余运算的说明:
- 如果 % 左边是正数,那么余数也是正数;
- 如果 % 左边是负数,那么余数也是负数;
100%12=4 100%-12=4 -100%12=-4 -100%-12=-4 |
赋值运算符
<<= | 左移且赋值运算符 | 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 | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %=>>= <<= &= ^= |= | 从右到左 |
逗号 | , | 从左到右 |
括号成员是老大; // 括号运算符 []() 成员运算符. -> 全体单目排老二; // 所有的单目运算符比如++、 --、 +(正)、 -(负) 、指针运算*、& 乘除余三,加减四; // 这个"余"是指取余运算即% 移位五,关系六; // 移位运算符:<< >> ,关系:> < >= <= 等 等与不等排行七; // 即 == 和 != 位与异或和位或; // 这几个都是位运算: 位与(&)异或(^)位或(|) "三分天下"八九十; 逻辑与,逻辑或; // 逻辑运算符: || 和 && 十一十二紧挨着; // 注意顺序: 优先级(||) 底于 优先级(&&) 条件只比赋值高, // 三目运算符优先级排到 13 位只比赋值运算符和 "," 高 逗号运算最低级! //逗号运算符优先级最低 |
类型转换
强制类型转换是把变量从一种类型转换为另一种数据类型。
int main() { int sum = 17, count = 5; double mean; mean = (double) sum / count; printf("Value of mean : %fn", mean ); } 当上面的代码被编译和执行时,它会产生下列结果: Value of mean : 3.400000 |
这里要注意的是强制类型转换运算符的优先级大于除法,因此 sum 的值首先被转换为 double 型,然后除以 count,得到一个类型为 double 的值。
类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。在编程时,有需要类型转换的时候都用上强制类型转换运算符,是一种良好的编程习惯。
int main() { int i = 17; char c = 'c'; /* ascii 值是 99 */ int sum; sum = i + c; printf("Value of sum : %dn", sum ); } |
当上面的代码被编译和执行时,它会产生下列结果:
Value of sum : 116
|
在这里,sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 ‘c’ 的值转换为对应的 ascii 值。
整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。
如果一个运算符两边的运算数类型不同,先要将其转换为相同的类型,即较低类型转换为较高类型,然后再参加运算,转换规则如下图所示。
判断循环
if-elseif
int main(void) { int n=10; if(n>10){ if((n%4==0&&n%100!=)||n%400==0){ printf("%d",n); } else{ return ((n>0)&&(n&(n-1))==0); } } else if(n=10){ return true; } else{ return false; } return 0; } |
for
for (语句1; 语句2; 语句3) { 被执行的代码块 } 语句 1 在循环(代码块)开始前执行 语句 2 定义运行循环(代码块)的条件 语句 3 在循环(代码块)已被执行之后执行(这就是循环中的++i和i++结果一样的原因,但是性能不一样,稍后解释) |
根据上面的for循环的语法定义 ++i 和 i++的结果是一样的,都要等代码块执行完毕才能执行语句3,但是性能是不同的。在大量数据的时候++i的性能要比i++的性能好原因:
i++由于是在使用当前值之后再+1,所以需要一个临时的变量来转存。
而++i则是在直接+1,省去了对内存的操作的环节,相对而言能够提高性能。
函数
函数声明
int max(int num1, int num2); //在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明: int max(int, int); |
函数传参
调用类型 | 描述 |
---|---|
传值调用 | 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。 |
引用调用 | 通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。 |
//引用调用 void swap(int *x, int *y) { int temp; temp = *x; /* 保存地址 x 的值 */ *x = *y; /* 把 y 赋值给 x */ *y = temp; /* 把 temp 赋值给 y */ return; } int main(void) { int x=1,y=2; swap(&x,&y); printf("x=%d,y=%d",x,y); return 0; } |
宏函数
在软件开发过程中,经常有一些常用或者通用的功能或者代码段,这些功能既可以写成函数,也可以封装成为宏定义。那么究竟是用函数好,还是宏定义好?这就要求我们对二者进行合理的取舍。
我们来看一个例子,比较两个数或者表达式大小,首先我们把它写成宏定义:
|
其次,把它用函数来实现:
int max( int a, int b) { return (a > b a : b) } |
很显然,我们不会选择用函数来完成这个任务,原因有两个:
首先,函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;
其次,函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。
和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。
看下面的例子:
( (type *) malloc((n)* sizeof(type))) |
利用这个宏,我们就可以为任何类型分配一段我们指定的空间大小,并返回指向这段空间的指针。我们可以观察一下这个宏确切的工作过程:
int *ptr; ptr = MALLOC ( 5, int ); 将这宏展开以后的结果: ptr = (int *) malloc ( (5) * sizeof(int) ); |
这个例子是宏定义的经典应用之一,完成了函数不能完成的功能,但是宏定义也不能滥用,通常,如果相同的代码需要出现在程序的几个地方,更好的方法是把它实现为一个函数。
example: define的单行定义 define的多行定义 define可以替代多行的代码,例如MFC中的宏定义(非常的经典,虽然让人看了恶心) stmt1; stmt2; } while(0) 关键是要在每一个换行的时候加上一个 " " //宏定义写出swap(x,y)交换函数 x = x + y; y = x - y; x = x - y; |
随机函数
/* 设置种子 */ srand( (unsigned)time( NULL ) ); for ( i = 0; i < 10; ++i) { r[i] = rand(); printf( "r[%d] = %dn", i, r[i]); } |
- 上例是拿当前系统时间作为种子,由于时间是变化的,种子变化,可以产生不相同的随机数。计算机中的随机数实际上都不是真正的随机数,如果两次给的种子一样,是会生成同样的随机序列的。 所以,一般都会以当前的时间作为种子来生成随机数,这样更加的随机。
- 使用时,参数可以是unsigned型的任意数据,比如srand(10);
- 如果不使用srand,用rand()产生的随机数,在多次运行,结果是一样的。
数组
一维数组
//声明数组 double balance[10]; //初始化数组 double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0}; //如果您省略掉了数组的大小,数组的大小则为初始化时元素的个数 double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0}; |
多维数组
//声明数组 int a[5][10]; //二维数组本质上是一个一维数组的列表,5行10列 //初始化数组 int a[3][4] = { {0, 1, 2, 3} , /* 初始化索引号为 0 的行 */ {4, 5, 6, 7} , /* 初始化索引号为 1 的行 */ {8, 9, 10, 11} /* 初始化索引号为 2 的行 */ }; //内部嵌套的括号是可选的,下面的初始化与上面是等同的: int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; |
一维数组传参
如果您想要在函数中传递一个一维数组作为参数,您必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。
//形式参数是一个指针 void myFunction(int *param) { . } //形式参数是一个已定义大小的数组 void myFunction(int param[10]) { . } //形式参数是一个未定义大小的数组 void myFunction(int param[]) { . } |
/* 函数声明 */ double getAverage(int arr[], int size); int main () { /* 带有 5 个元素的整型数组 */ int balance[5] = {1000, 2, 3, 17, 50}; double avg; /* 传递一个指向数组的指针作为参数 */ avg = getAverage( balance, 5 ) ; /* 输出返回值 */ printf( "平均值是: %f ", avg ); return 0; } double getAverage(int arr[], int size) { int i; double avg; double sum=0; for (i = 0; i < size; ++i) { sum += arr[i]; } avg = sum / size; return avg; } |
就函数而言,数组的长度是无关紧要的,因为 C 不会对形式参数执行边界检查。
二维数组传参
方法1: 第一维的长度可以不指定,但必须指定第二维的长度:
void print_a(int a[][5], int n, int m)
|
方法2: 指向一个有5个元素一维数组的指针:
void print_b(int (*a)[5], int n, int m)
|
方法3: 利用数组是顺序存储的特性,通过降维来访问原数组!
void print_c(int *a, int n, int m)
|
如果知道二维数组的长度,当然选择第一或者第二种方式,但是长度不确定时,只能传入数组大小来遍历元素啦。
/********************************* * 方法1: 第一维的长度可以不指定 * 但必须指定第二维的长度 *********************************/ void print_a(int a[][5], int n, int m){ int i, j; for(i = 0; i < n; i++) { for(j = 0; j < m; j++) printf("%d ", a[i][j]); printf("n"); } } /***************************************** * 方法2: 指向一个有5个元素一维数组的指针 *****************************************/ void print_b(int (*a)[5], int n, int m) { int i, j; for(i = 0; i < n; i++) { for(j = 0; j < m; j++) printf("%d ", a[i][j]); printf("n"); } } /*********************************** * 方法3: 利用数组是顺序存储的特性, * 通过降维来访问原数组! ***********************************/ void print_c(int *a, int n, int m) { int i, j; for(i = 0; i < n; i++) { for(j = 0; j < m; j++) printf("%d ", *(a + i*m + j)); printf("n"); } } int main(void) { int a[5][5] = {{1, 2}, {3, 4, 5}, {6}, {7}, {0, 8}}; printf("n方法1:n"); print_a(a, 5, 5); printf("n方法2:n"); print_b(a, 5, 5); printf("n方法3:n"); print_c(&a[0][0], 5, 5); // getch(); return 0; } |
数组返参
如果您想要从函数返回一个一维数组,您必须声明一个返回指针的函数,如下:
int * myFunction() { . } |
另外,C 不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。
/* 要生成和返回随机数的函数 */ int * getRandom( ) { static int r[10]; int i; /* 设置种子 */ srand( (unsigned)time( NULL ) ); for ( i = 0; i < 10; ++i) { r[i] = rand(); printf( "r[%d] = %dn", i, r[i]); } return r; } /* 要调用上面定义函数的主函数 */ int main () { /* 一个指向整数的指针 */ int *p; int i; p = getRandom(); for ( i = 0; i < 10; i++ ) { printf( "*(p + %d) : %dn", i, *(p + i)); } return 0; } |
指针
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。
使用指针
int main () { int var = 20; /* 实际变量的声明 */ int *ip; /* 指针变量的声明 */ ip = &var; /* 在指针变量中存储 var 的地址 */ printf("Address of var variable: %pn", &var ); /* 在指针变量中存储的地址 */ printf("Address stored in ip variable: %pn", ip ); /* 使用指针访问值 */ printf("Value of *ip variable: %dn", *ip ); return 0; } |
结果为:
Address of var variable: bffd8b3c Address stored in ip variable: bffd8b3c Value of *ip variable: 20 |
NULL
指针
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。
NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:
int main () { int *ptr = NULL; printf("ptr 的地址是 %pn", ptr ); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
ptr 的地址是 0x0
|
如果指针包含空值(零值),则假定它不指向任何东西。
如需检查一个空指针,您可以使用 if 语句,如下所示:
if(ptr) /* 如果 p 非空,则完成 */ if(!ptr) /* 如果 p 为空,则完成 */ |
算术运算
递增递减
我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:
const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i, *ptr; /* 指针中的数组地址 */ ptr = var; //ptr = &var[MAX-1]; for ( i = 0; i < MAX; i++) //for ( i = MAX; i > 0; i--) { printf("存储地址:var[%d] = %xn", i, ptr ); printf("存储值:var[%d] = %dn", i, *ptr ); /* 移动到下一个位置 */ ptr++; //ptr--; } return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
存储地址:var[0] = bf882b30 存储值:var[0] = 10 存储地址:of var[1] = bf882b34 存储值: var[1] = 100 存储地址:of var[2] = bf882b38 存储值:var[2] = 200 |
指针比较
即指针变量与指针变量,指针变量与变量地址的比较!
const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i, *ptr; /* 指针中第一个元素的地址 */ ptr = var; i = 0; while ( ptr <= &var[MAX - 1] ) { printf("Address of var[%d] = %xn", i, ptr ); printf("Value of var[%d] = %dn", i, *ptr ); /* 指向上一个位置 */ ptr++; i++; } return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Address of var[0] = bfdbcb20 Value of var[0] = 10 Address of var[1] = bfdbcb24 Value of var[1] = 100 Address of var[2] = bfdbcb28 Value of var[2] = 200 |
二级指针
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
int main () { int var; int *ptr; int **pptr; //声明二级指针 var = 3000; /* 获取 var 的地址 */ ptr = &var; /* 使用运算符 & 获取 ptr 的地址 */ pptr = &ptr; /* 使用 pptr 获取值 */ printf("Value of var = %dn", var ); printf("Value available at *ptr = %dn", *ptr ); printf("Value available at **pptr = %dn", **pptr); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Value of var = 3000 Value available at *ptr = 3000 Value available at **pptr = 3000 |
常量指针
常量指针又叫常指针,可以理解为常量的指针,指向的是个常量!
代码形式:
int const* p; const int* p; |
关键点:
- 常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
- 常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
- 指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;
int main() { int i = 10; int i2 = 11; const int *p = &i; printf("%dn", *p);//10 i = 9; //OK,仍然可以通过原来的声明修改值, *p = 11; //Error,*p是const int的,不可修改,即常量指针不可修改其指向地址 p = &i2; //OK,指针还可以指向别处,因为指针只是个变量,可以随意指向; printf("%dn", *p);//11 return 0; } |
指针常量
本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。
代码形式:
int* const p;
|
关键点:
- 它是个常量!
- 指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
- 指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;
int main() { int i = 10; int *const p = &i; printf("%dn", *p);//10 p++; //Error,因为p是const 指针,因此不能改变p指向的内容 (*p)++; //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化 printf("%dn", *p);//11 i = 9;//OK,仍然可以通过原来的声明修改值, return 0; } |
归纳小记:
const int a
也可以写成int const a
,因为int
和const
都作为一个类型限定词,有相同的地位。区分常量指针和指针常量:
- 一种方式是看
*
和 const 的排列顺序,比如
> int const* p; //const * 即常量指针 > const int* p; //const * 即常量指针 > int* const p; //* const 即指针常量 > >
- 还一种方式是看const离谁近,即从右往左看,比如
> int const* p; //const修饰的是*p,即*p的内容不可通过p改变,但p不是const,p可以修改,*p不可修改; > const int* p; //同上 > int* const p; //const修饰的是p,p是指针,p指向的地址不能修改,p不能修改,但*p可以修改; > >
通过定义指针变量可以修改const常量,强制类型转换后不会报错,如下
> > int main() { > const int a; > int const a2; > int *pi = (int *) &a; > *pi = 19; > printf("%dn", a);//19 > pi = (int *) &a2; > *pi = 20; > printf("%dn", a2);//20 > return 0; > } > >
指向常量的常指针
定义:
指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。
关键点:
- 一个指针常量,指向的是一个指针对象;
- 它指向的指针对象且是一个常量,即它指向的对象不能变化;
代码形式:
const int* const p;
|
int main() { int i = 10; const int *const p = &i; printf("%dn", *p);//10 p++;//error: increment of read-only variable ‘p’ (*p)++;//increment of read-only location ‘*p’ i++;//OK,仍然可以通过原来的声明修改值 printf("%dn", *p);//11 return 0; } |
指针数组
英文解释为array of pointers,即用于存储指针的数组,也就是数组元素都是指针!
int *ptr[MAX]; //指针数组 //把 ptr 声明为一个数组,由 MAX 个整数指针组成 //ptr 中的每个元素,都是一个指向 int 值的指针 //元素表示:*a[i] *(a[i])是一样的,因为[]优先级高于* |
在实际应用中,对于指针数组,我们经常这样使用:
typedef int* pInt; pInt a[4]; |
如下实例:
const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i, *ptr[MAX]; for ( i = 0; i < MAX; i++) { ptr[i] = &var[i]; /* 赋值为整数的地址 */ } for ( i = 0; i < MAX; i++) { printf("Value of var[%d] = %dn", i, *ptr[i] ); } return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Value of var[0] = 10 Value of var[1] = 100 Value of var[2] = 200 |
如下实例:
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] = %sn", i, names[i] ); } return 0; } |
Value of names[0] = Zara Ali Value of names[1] = Hina Ali Value of names[2] = Nuha Ali Value of names[3] = Sara Ali |
指针数组与数组指针:
int main() { int c[4]={1,2,3,4}; int *a[4]; //指针数组 int (*b)[4]; //数组指针 b=&c; //将数组c中元素赋给数组a for(int i=0;i<4;i++) { a[i]=&c[i]; } //输出看下结果 printf("%d",*a[1]); //输出2就对 printf("%d",(*b)[2]); //输出3就对 return 0; } |
注意:定义了数组指针,该指针指向这个数组的首地址,必须给指针指定一个地址,容易犯的错得就是,不给b地址,直接用
(*b)[i]=c[i]给数组b中元素赋值,这时数组指针不知道指向哪里,调试时可能没错,但运行时肯定出现问题,使用指针时要注意这个问题。
但为什么a就不用给他地址呢,a的元素是指针,实际上for循环内已经给数组a中元素指定地址了。但若在for循环内写
*a[i]=c[i]
,这同样会出问题。总之一句话,定义了指针一定要知道指针指向哪里,不然要悲剧。
数组指针
英文解释为a pointer to an array,即指向数组的指针!
int (*a)[4] //数组指针 //表示:指向数组a的指针 //元素表示:(*a)[i] |
int main () { /* 带有 5 个元素的整型数组 */ double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0}; double *p; int i; p = balance; //p和balance为指向&balance[0]的指针 /* 输出数组中每个元素的值 */ printf( "使用指针的数组值n"); for ( i = 0; i < 5; i++ ) { printf("*(p + %d) : %fn", i, *(p + i) ); //*(p + i) == p[i] } printf( "使用 balance 作为地址的数组值n"); for ( i = 0; i < 5; i++ ) { printf("*(balance + %d) : %fn", i, *(balance + i) ); } return 0; } |
指针数组和数组指针的区别:
指针数组
指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组。
其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型。
在 32 位系统中,指针占四个字节。
数组指针
数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针。
其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
根据上面的解释,可以了解到指针数组和数组指针的区别,因为二者根本就是种类型的变量。
函数指针
是一个指针,只不过这个指针指向函数地址,他和其他函数名一样,具有(返回值类型,形参个数和类型)
函数指针变量的声明:
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
|
以下实例声明了函数指针变量 p,指向函数 max:
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("最大的数字是: %dn", d); return 0; } |
编译执行,输出结果如下:
请输入三个数字:1 2 3 最大的数字是: 3 |
指针函数
是一个函数,只不过和一般函数区分的原因是它返回的是一个指针。
int* f ( int , int ) ; // 返回的是一个整形指针 int f ( int, int); // 返回的是一个整形数 |
当然,指针函数在使用时,必须与调用者的类型相同, 也就是说,返回值必须和左值的类型相同。
/* * 指针函数的定义 * 返回值是指针类型int * */ int *f(int a, int b) { int *p = (int *)malloc(sizeof(int)); memset(p, 0, sizeof(int)); *p = a + b; return p; } int main(){ int *p1 = NULL; p1 = f(1, 2); //类型相同 |
归纳小记:
类型 | 描述 |
---|---|
数组指针 | int (*p)[10] ;一个指针,它指向的是一个数组 |
指针数组 | int *p[10] ; 一个数组,它包含的元素是指针 |
函数指针 | int (*p)(int,int) ;一种特殊的指针,它指向函数的入口 |
指针函数 | int *p(int,int) ;一个函数,它的返回值是指针 |
回调函数
函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
简单讲:回调函数是由别人的函数执行时调用你实现的函数。
如下实例:
int sum(int a,int b){ return a+b; } //sum2回调sum函数 int sum2(int num,int (*sum)(int,int),int a,int b){ return num * sum(a,b); } int main() { int (* p)(int a,int b)=sum; printf("SUM=%dn",sum(1,2)); printf("SUM2= %dn",sum2(2,sum,1,2)); return 0; } |
编译执行,输出结果如下:
SUM=3 SUM2= 6 |
指针形参
C 语言允许您传递指针给函数,只需要简单地声明函数参数为指针类型即可。
void getSeconds(unsigned long *par); int main () { unsigned long sec; /* 带有 5 个元素的整型数组 */ int balance[5] = {1000, 2, 3, 17, 50}; getSeconds( &sec ); /* 传递一个指向数组的指针作为参数 */ avg = getAverage( balance, 5 ) ; /* 输出实际值 */ printf("Number of seconds: %ldn", sec ); printf("Average value is: %fn", avg ); return 0; } void getSeconds(unsigned long *par) { /* 获取当前的秒数 */ *par = time( NULL ); return; } 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; } |
指针返参
C 允许您从函数返回指针。为了做到这点,您必须声明一个返回指针的函数,如下所示:
int * myFunction() { . } |
下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们,具体如下:
/* 要生成和返回随机数的函数 */ int * getRandom( ) { static int r[10]; int i; /* 设置种子 */ srand( (unsigned)time( NULL ) ); for ( i = 0; i < 10; ++i) { r[i] = rand(); printf("%dn", r[i] ); } return r; } /* 要调用上面定义函数的主函数 */ int main () { /* 一个指向整数的指针 */ int *p; int i; p = getRandom(); for ( i = 0; i < 10; i++ ) { printf("*(p + [%d]) : %dn", i, *(p + i) ); } return 0; } |
注意:C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。
因为局部变量是存储在内存的栈区内,当函数调用结束后,局部变量所占的内存地址便被释放了,因此当其函数执行完毕后,函数内的变量便不再拥有那个内存地址,所以不能返回其指针。
除非将其变量定义为 static 变量,static 变量的值存放在内存中的静态数据区,不会随着函数执行的结束而被清除,故能返回其地址。
字符串
字符串实际上是使用 null 字符 ‘’ 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。
初始化
下面的声明和初始化创建了一个 “Hello” 字符串。由于在数组的末尾存储了空字符,所以字符数组的大小比单词 “Hello” 的字符数多一个。
char greeting[6] = {'H', 'e', 'l', 'l', 'o', ''}; printf("Greeting message: %sn", greeting ); |
依据数组初始化规则,您可以把上面的语句写成以下语句:
char greeting[] = "Hello"; printf("Greeting message: %sn", greeting ); |
其实,您不需要把 null 字符放在字符串常量的末尾。C 编译器会在初始化数组时,自动把 ‘’ 放在字符串的末尾。
字符串函数
函数 | 描述 |
---|---|
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 的第一次出现的位置。 |
int main() { char str1[12] = "Hello"; char str2[12] = "World"; char str3[12]; int len,size; /* 复制 str1 到 str3 */ strcpy(str3, str1); printf("strcpy( str3, str1): %sn", str3 ); /* 连接 str1 和 str2 */ strcat( str1, str2); printf("strcat( str1, str2): %sn", str1 ); /* 连接后,str1 的总长度 */ len = strlen(str1); size = sizeof(str1); printf("strlen(str1): %dn", len ); printf("sizeof(str1): %dn", size ); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
strcpy( str3, str1): Hello strcat( str1, str2): HelloWorld strlen(str1): 10 sizeof(str1): 12 |
注意:
strlen 是函数,sizeof 是运算操作符,二者得到的结果类型为 size_t,即 unsigned int 类型。
sizeof 计算的是变量的大小,不受字符
而 strlen 计算的是字符串的长度,以 作为长度判定依据。
格式化字符串
下面是 printf() 函数的声明:
int printf(const char *format, ...)
|
format 包含了要被写入到标准输出 stdout 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。
format 标签属性是 %[flags][width][.precision][length]specifier
,具体讲解如下:
格式字符 | 意义 |
---|---|
d | 以十进制形式输出带符号整数(正数不输出符号) |
o | 以八进制形式输出无符号整数(不输出前缀0) |
x,X | 以十六进制形式输出无符号整数(不输出前缀Ox) |
u | 以十进制形式输出无符号整数 |
f | 以小数形式输出单、双精度实数 |
e,E | 以指数形式输出单、双精度实数 |
g,G | 以%f或%e中较短的输出宽度输出单、双精度实数 |
c | 输出单个字符 |
s | 输出字符串 |
p | 输出指针地址 |
lu | 32位无符号整数 |
llu | 64位无符号整数 |
length(长度) | 描述 |
---|---|
h | 参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。 |
l | 参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。 |
L | 参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。 |
int main() { char ch = 'A'; char str[20] = "www.runoob.com"; float flt = 10.234123123123; int no = 150; double dbl = 20.123456; printf("字符为 %c n", ch); printf("字符串为 %s n" , str); printf("浮点数为 %f n", flt); printf("浮点数为 %.8f n", flt); printf("浮点数为 %16.8f n", flt); //输出8位小数,占16列,右对齐 printf("浮点数为 %-16.8f n", flt); //输出8位小数,占16列,左对齐 printf("整数为 %dn" , no); printf("双精度值为 %lf n", dbl); printf("八进制值为 %o n", no); printf("十六进制值为 %x n", no); return 0; } |
执行输出结果为:
字符为 A 字符串为 www.runoob.com 浮点数为 10.234123 浮点数为 10.23412323 浮点数为 10.23412323 浮点数为 10.23412323 整数为 150 双精度值为 20.123456 八进制值为 226 十六进制值为 96 |
结构体
C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。
定义结构
struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book; |
解释:
Books是结构体标签
book是结构变量,可以通过
,
指定一个或多个结构变量一般情况下,结构体标签和结构变量至少出现一个!
定义结构体的几种方法:
struct { int a; char b; double c; } s1; |
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 COMPLEX { char string[100]; struct SIMPLE a; }; //此结构体的声明包含了指向自己类型的指针 struct NODE { char string[100]; struct NODE *next_node; }; |
如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:
struct B; //对结构体B进行不完整声明 //结构体A中包含指向结构体B的指针 struct A { struct B *partner; //other members; }; //结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明 struct B { struct A *partner; //other members; }; |
初始化结构体
和其它类型变量一样,对结构体变量可以在定义时指定初始值。
struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book = {"C 语言", "RUNOOB", "编程语言", 123456}; int main() { struct Books s,s1; strcpy(s.title,"C++"); strcpy(s.author,"jy"); strcpy(s.subject,"编程语言"); s.book_id=111111; scanf("%s",s1.title); scanf("%s",s1.author); scanf("%s",s1.subject); scanf("%d",s1.book_id); printf("title : %snauthor: %snsubject: %snbook_id: %dn", book.title, book.author, book.subject, book.book_id); printf("title : %snauthor: %snsubject: %snbook_id: %dn", s.title, s.author, s.subject, s.book_id); } |
访问成员
为了访问结构的成员,我们使用成员访问运算符(.)
struct Books { char title[50]; char author[50]; char subject[100]; int book_id; }; 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 信息 */ printf( "Book 1 title : %sn", Book1.title); printf( "Book 1 author : %sn", Book1.author); printf( "Book 1 subject : %sn", Book1.subject); printf( "Book 1 book_id : %dn", Book1.book_id); /* 输出 Book2 信息 */ printf( "Book 2 title : %sn", Book2.title); printf( "Book 2 author : %sn", Book2.author); printf( "Book 2 subject : %sn", Book2.subject); printf( "Book 2 book_id : %dn", Book2.book_id); return 0; } |
结构体传参
您可以把结构作为函数参数,传参方式与其他类型的变量或指针类似。您可以使用上面实例中的方式来访问结构变量:
struct Books { char title[50]; char author[50]; char subject[100]; int book_id; }; /* 函数声明 */ void printBook( struct Books book ); int main( ) { struct Books Book; /* 声明 Book,类型为 Books */ strcpy( Book.title, "C Programming"); strcpy( Book.author, "Nuha Ali"); strcpy( Book.subject, "C Programming Tutorial"); Book.book_id = 6495407; printBook( Book ); return 0; } void printBook( struct Books book ) { printf( "Book title : %sn", book.title); printf( "Book author : %sn", book.author); printf( "Book subject : %sn", book.subject); printf( "Book book_id : %dn", book.book_id); } |
指向结构的指针
您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似,如下所示:
struct Books *struct_pointer;
|
现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示:
struct_pointer = &Book1;
|
为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:
struct_pointer->title;
|
让我们使用结构指针来重写上面的实例,这将有助于您理解结构指针的概念:
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 */ /* Book 详述 */ strcpy( Book.title, "C Programming"); strcpy( Book.author, "Nuha Ali"); strcpy( Book.subject, "C Programming Tutorial"); Book.book_id = 6495407 /* 通过传 Book 的地址来输出 Book1 信息 */ printBook( &Book ); return 0; } void printBook( struct Books *book ) { printf( "Book title : %sn", book->title); printf( "Book author : %sn", book->author); printf( "Book subject : %sn", book->subject); printf( "Book book_id : %dn", book->book_id); } |
结构体数组
一个结构体变量中可以存放一组数据(如一个学生的学号,姓名,成绩等数据)。如果有10个学生的数据需要参加运算,显然应该用数组,这就是结构体数组。
结构体数组与以前介绍过的数据值型数组不同之处在于每个数组元素都一个结构体类型的数据,它们分别包括各个成员(分量)项。
定义结构体数组
struct student { int num; char name[20]; char sex; int age; float score; char addr[30]; }; struct student stu[3]; //*************************// struct student { int num; .... }stu[3]; //*************************// struct { int num; ... }stu[3]; |
初始化:
struct student { int mum; char name[20]; char sex; int age; float score; char addr[30]; }stu[3] = {{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}, {10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}, {10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}}; |
定义数组 stu 时,元素个数可以不指定,即写成以下形式:
stu[] = {{...},{...},{...}};
|
编译时,系统会根据给出初值的结构体常量的个数来确定数组元素的个数。
当然,数组的初始化也可以用以下形式:
struct student { int num; ... }; struct student stu[] = {{...},{...},{...}}; |
即先声明结构体类型,然后定义数组为该结构体类型,在定义数组时初始化。
从以上可以看到,结构体数组初始化的一般形式是在定义数组的后面加上:
struct person { char name[20]; int count; }leader[3] = {{"Li", 0}, {"Zhang", 0}, {"Fun", 0}}; void main() { int i, j; char leader_name[20]; for(i = 1; i<= 10;i++) { scanf("%s", leader_name); for(j=0;j<3;j++) if(strcmp(leader_name, leader[j].name) == 0) leader[j].count ++; } printf("n"); for(i=0;i<3;i++) printf("%5s: %dn", leader[i].name, leader[i].count); system("pause"); } |
结构体对齐
结构体中成员变量分配的空间是按照成员变量中占用空间最大的来作为分配单位
同样成员变量的存储空间也是不能跨分配单位的,如果当前的空间不足,则会存储到下一个分配单位中。
typedef struct { unsigned char a; unsigned int b; unsigned char c; } debug_size1_t; typedef struct { unsigned char a; unsigned char b; unsigned int c; } debug_size2_t; int main(void) { printf("debug_size1_t size=%lu,debug_size2_t size=%lurn", sizeof(debug_size1_t), sizeof(debug_size2_t)); return 0; } |
编译执行输出结果:
debug_size1_t size=12,debug_size2_t size=8
|
结构体占用存储空间,以32位机为例
- 1.debug_size1_t 存储空间分布为a(1byte)+空闲(3byte)+b(4byte)+c(1byte)+空闲(3byte)=12(byte)。
- 1.debug_size2_t 存储空间分布为a(1byte)+b(1byte)+空闲(2byte)+c(4byte)=8(byte)。
位域
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。
例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。
为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为”位域”或”位段”。
所谓”位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
struct { unsigned int widthValidated : 1; unsigned int heightValidated : 1; } status; |
现在,上面的结构中,status 变量将占用 4 个字节的内存空间,但是只有 2 位被用来存储值。
如果您用了 32 个变量,每一个变量宽度为 1 位,那么 status 结构将使用 4 个字节,但只要您再多用一个变量,如果使用了 33 个变量,那么它将分配内存的下一段来存储第 33 个变量,这个时候就开始使用 8 个字节。
位域的定义方式如下:
struct bs{ int a:8; int b:2; int c:6; }data; |
说明 data 为 bs 变量,共占两个字节。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。
/* 定义简单的结构 */ struct { unsigned int widthValidated; unsigned int heightValidated; } status1; /* 定义位域结构 */ struct { unsigned int widthValidated : 1; unsigned int heightValidated : 1; } status2; int main( ) { printf( "Memory size occupied by status1 : %dn", sizeof(status1)); printf( "Memory size occupied by status2 : %dn", sizeof(status2)); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Memory size occupied by status1 : 8 Memory size occupied by status2 : 4 |
struct { unsigned int age : 3; } Age; int main( ) { Age.age = 4; printf( "Sizeof( Age ) : %dn", sizeof(Age) ); printf( "Age.age : %dn", Age.age ); Age.age = 7; printf( "Age.age : %dn", Age.age ); Age.age = 8; // 二进制表示为 1000 有四位,超出,则直接丢掉了,存不进去。 printf( "Age.age : %dn", Age.age ); return 0; } |
当上面的代码被编译时,它会带有警告,当上面的代码被执行时,它会产生下列结果:
Sizeof( Age ) : 4 Age.age : 4 Age.age : 7 Age.age : 0 |
对于位域的定义尚有以下几点说明:
- 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct bs{ unsigned a:4; unsigned :4; /* 空域 */ unsigned b:4; /* 从下一单元开始存放 */ unsigned c:4 }
在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。
- 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。如果最大长度大于计算机的整数字长,一些编译器可能会允许域的内存重叠,另外一些编译器可能会把大于一个域的部分存储在下一个字中。
- 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
struct k{ int a:1; int :2; /* 该 2 位不能使用 */ int b:3; int c:2;};
从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。
位域的使用:
位域的使用和结构成员的使用相同,其一般形式为:
位域变量名.位域名 位域变量名->位域名 |
位域允许用各种格式输出。
int main() { struct bs{ unsigned a:1; unsigned b:3; unsigned c:4; } bit,*pbit; bit.a=1; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */ bit.b=7; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */ bit.c=15; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */ printf("%d,%d,%dn",bit.a,bit.b,bit.c); /* 以整型量格式输出三个域的内容 */ pbit=&bit; /* 把位域变量 bit 的地址送给指针变量 pbit */ pbit->a=0; /* 用指针方式给位域 a 重新赋值,赋为 0 */ pbit->b&=3; /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */ pbit->c|=1; /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */ printf("%d,%d,%dn",pbit->a,pbit->b,pbit->c); /* 用指针方式输出了这三个域的值 */ } |
1,7,15 0,3,15 |
上例程序中定义了位域结构 bs,三个位域为 a、b、c。说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit。这表示位域也是可以使用指针的。
联合枚举
联合
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。
您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
定义联合体:
union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:
union [union tag] { member definition; member definition; ... member definition; } [one or more union variables]; |
union Data { int i; float f; char str[20]; } data; |
现在,Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。
共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。
下面的实例将显示上面的共用体占用的总内存大小:
union Data { int i; float f; char str[20]; }; int main( ) { union Data data; printf( "Memory size occupied by data : %dn", sizeof(data)); return 0; } //Memory size occupied by data : 20 |
成员访问
与结构体访问成员对象的方法一致。
union Data { int i; float f; char str[20]; }; int main( ) { union Data data; data.i = 10; data.f = 220.5; strcpy( data.str, "C Programming"); printf( "data.i : %dn", data.i); printf( "data.f : %fn", data.f); printf( "data.str : %sn", data.str); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 1917853763 data.f : 4122360580327794860452759994368.000000 data.str : C Programming |
在这里,我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。
现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:
union Data { int i; float f; char str[20]; }; int main( ) { union Data data; data.i = 10; printf( "data.i : %dn", data.i); data.f = 220.5; printf( "data.f : %fn", data.f); strcpy( data.str, "C Programming"); printf( "data.str : %sn", data.str); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 10 data.f : 220.500000 data.str : C Programming |
在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。
判断大端小端
union { char str; int data; }; data=0x01020304; if(str==0x01) { cout<< "此机器是大端!"<<endl; } else if(str==0x04){ cout<<"此机器是小端!"<<endl; } else{ cout <<" 暂无法判断此机器类型!"<<endl; } |
注:大端机高位存在低位,小端机反之!
归纳总结**
结构体变量所占内存长度是其中最大字段大小的整数倍。
共用体变量所占的内存长度等于最长的成员变量的长度。
共用体作用:节省内存,有两个很长的数据结构,不会同时使用,比如一个表示老师,一个表示学生,如果要统计教师和学生的情况用结构体的话就有点浪费了!用共用体的话,只占用最长的那个数据结构所占用的空间,就足够了!
通信中的数据包会用到共用体:因为不知道对方会发一个什么包过来,用共用体的话就很简单了,定义几种格式的包,收到包之后就可以直接根据包的格式取出数据。
共用体和结构体一样,存在内存对齐,以8个字节为一组,多余的存放在下一组。
枚举
类型声明
枚举语法定义格式为:
enum 枚举名 { 枚举元素1,枚举元素2,…… }; |
如:
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN }; |
等价于:
|
注意:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推;没有指定值的枚举元素,其值为前一元素加 1。
变量定义
我们可以通过以下三种方式来定义枚举变量
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; |
遍历枚举元素
在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。
不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN } day; int main() { // 遍历枚举元素 for (day = MON; day <= SUN; day++) { printf("枚举元素:%d n", day); } } |
以上实例输出结果为:
枚举元素:1 枚举元素:2 枚举元素:3 枚举元素:4 枚举元素:5 枚举元素:6 枚举元素:7 |
注意:遍历枚举类型是,枚举元素的值不连续是不可行的,如下:
int main(void) { enum MONTH{A=1,B,C,D,F=10}; //注意B的值是在A加1,C是B加1....所以D你知道吧 enum MONTH month=A; for(month=A;month<F;month++) printf("%d ",month); return 0; } |
得到的结果:
1 2 3 4 5 6 7 8 9
|
它只是给 day 赋值了一个整数类型的值。
switch
int main() { enum color { red=1, green, blue }; enum color favorite_color; /* ask user to choose color */ printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): "); scanf("%d", &favorite_color); /* 输出结果 */ switch (favorite_color) { case red: printf("你喜欢的颜色是红色"); break; case green: printf("你喜欢的颜色是绿色"); break; case blue: printf("你喜欢的颜色是蓝色"); break; default: printf("你没有选择你喜欢的颜色"); } return 0; } |
以上实例输出结果为: 请输入你喜欢的颜色: (1. red, 2. green, 3. blue): 1 你喜欢的颜色是红色 |
类型转换
以下实例将整数转换为枚举:
实例
以下实例将整数转换为枚举:
int main() { enum day { saturday, sunday, monday, tuesday, wednesday, thursday, friday } workday; int a = 1; enum day weekend; weekend = ( enum day ) a; //类型转换 //weekend = a; //错误 printf("weekend:%d",weekend); return 0; } |
以上实例输出结果为:
weekend:1
|
输入输出
C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 您的屏幕 |
注:文件指针是访问文件的方式
C 语言中的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数。
scanf() 函数用于从标准输入(键盘)读取并格式化, printf() 函数发送格式化输出到标准输出(屏幕)。
int main() { float f; printf("Enter a number: "); // %f 匹配浮点型数据 scanf("%f",&f); printf("Value = %f", f); return 0; } |
getchar()
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
putchar()
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。
int main( ) { int c; printf( "Enter a value :"); c = getchar( ); printf( "nYou entered: "); putchar( c ); printf( "n"); return 0; } |
当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并只会读取一个单一的字符,显示如下:
$./a.out Enter a value :runoob You entered: r |
gets()
char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
puts()
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。
int main( ) { char str[100]; printf( "Enter a value :"); gets( str ); printf( "nYou entered: "); puts( str ); return 0; } |
当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取一整行直到该行结束,显示如下:
$./a.out Enter a value :runoob You entered: runoob |
scanf()
int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。
输入缓冲区
int i; char c; scanf("%d%c", &i,&c); |
这时候变量 c 中存储的往往不是你想输入的字符,而是一个空格,然后我们又会这样来写:
int i; char c; scanf("%d", &i); scanf("%c", &c); |
这时候,我们发现,根本没有输入字符C的机会,这是为什么?因为输入流是有缓冲区的,我们输入的字符存储在那,然后再赋值给我们的变量。我们可以这样改:
int i; char c; scanf("%d", &i); while((c=getchar())==' ' || c=='n'); c = getchar(); |
这个办法是一直读取,读到没有空格和换行就跳出循环,但是有一个更好的解决办法;
int i; char c; scanf("%d%[^' '^'n']", &i, &c); |
这是用正则表达来控制输入格式为非空格非换行。
在输入时注意格式:
int main() { int a; float x; char c1; scanf("a=%d",&a); scanf("x=%f",&x); scanf("c1=%c",&c1); printf("a=%d,x=%f,c1=%c",a,x,c1); return 0; } |
若在输入时用错空格键或者换行符,则会出现错误:
a=1 x=1.2 c1=3
|
上述输入只能输出 a=1 因为空格键取代了 x 的位置 输入完 x=1.2 后空格键有取代了应该输入 c1 的位置。
正确的输入应为:
a=1x=1.2c1=3
|
scanf的返回值:
int main() { int a; int b; int c; printf("请输入三个整数:"); int x=scanf("%d%d%d",&a,&b,&c); printf("d%n%dn",a,x); } |
测试输出:
$ ./a.out 请输入三个整数:1 2 3 1 3 $ ./a.out 请输入三个整数:5 6 d 5 2 |
- 1、scanf() 函数有返回值且类型 int 型,当发生错误时立刻返回 EOF。
- 2、scanf() 函数返回的值为:正确按指定格式输入变量的个数;也即能正确接收到值的变量个数。
printf()
int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。
format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。
int main( ) { char str[100]; int i; printf( "Enter a value :"); scanf("%s %d", str, &i); printf( "nYou entered: %s %d ", str, i); printf("n"); return 0; } |
当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取输入,显示如下:
$./a.out Enter a value :runoob 123 You entered: runoob 123 |
在这里,应当指出的是,scanf() 期待输入的格式与您给出的 %s 和 %d 相同,这意味着您必须提供有效的输入,比如 “string integer”,如果您提供的是 “string string” 或 “integer integer”,它会被认为是错误的输入。另外,在读取字符串时,只要遇到一个空格,scanf() 就会停止读取,所以 “this is test” 对 scanf() 来说是三个字符串。
fgets()
fgets函数原型:char *fgets(char *s, int n, FILE *stream);//我们平时可以这么使用:fgets(str, sizeof(str), stdin);
其中str为数组首地址,sizeof(str)为数组大小,stdin表示我们从键盘输入数据。
fgets函数功能:从文件指针stream中读取字符,存到以s为起始地址的空间里,直到读完N-1个字符,或者读完一行。
注意:调用fgets函数时,最多只能读入n-1个字符。读入结束后,系统将自动在最后加’’,并以str作为函数值返回。
int main( ) { char str[5]; printf( "Enter a value :"); gets( str ); printf( "nYou entered: "); puts( str ); return 0; } |
如果输入123(长度小于5)结果为:
Enter a value :123 You entered: 123 |
如果输入123456789(长度大于5)结果为:
Enter a value :123456789 You entered: 123456789 |
虽然正常显示了,但是系统提示程序崩溃了
如果不能正确使用gets()函数,带来的危害是很大的,就如上面我们看到的,输入字符串的长度大于缓冲区长度时,并没有截断,原样输出了读入的字符串,造成程序崩溃。
考虑到程序安全性和健壮性,建议用fgets()来代替gets()。如:
int main( ) { char str[5]; printf( "Enter a value :"); fgets( str,5,stdin ); //fgets()函数; printf( "nYou entered: "); puts( str ); return 0; } |
fputs()
int main() { char c[100]; printf("Enter a value:"); fgets( c,100,stdin ); printf("nyou entered:"); fputs( c,stdout ); return 0; } |
回车符和换行符
换行符‘n’和回车符‘r’
(1)换行符就是另起一行
(2)回车符就是回到一行的开头
所以我们平时编写文件的回车符应该确切来说叫做回车换行符
CR: 回车(Carriage Return) : r
LF: 换行(Line Feed) : n
不同系统的应用:
1)在微软的MS-DOS和Windows中,使用“回车CR(‘r’)”和“换行LF(‘n’)”两个字符作为换行符;
2)Windows系统里面,每行结尾是 回车+换行(CR+LF),即“rn”;
3)Unix系统里,每行结尾只有 换行LF,即“n”;
4)Mac系统里,每行结尾是 回车CR 即’r’。
注意:Mac OS 9 以及之前的系统的换行符是 CR,从 Mac OS X (后来改名为“OS X”)开始的换行符是 LF即‘n’,和Unix/Linux统一了。
影响:
(1)一个直接后果是,Unix/Mac系统下的文件在Windows里打开的话,所有文字会变成一行;
(2)而Windows里的文件在Unix/Mac下打开的话,在每行的结尾可能会多出一个^M符号。
(3)Linux保存的文件在windows上用记事本看的话会出现黑点。
相互转换:
在linux下,命令unix2dos 是把linux文件格式转换成windows文件格式,命令dos2unix 是把windows格式转换成linux文件格式。
在不同平台间使用FTP软件传送文件时, 在ascii文本模式传输模式下, 一些FTP客户端程序会自动对换行格式进行转换. 经过这种传输的文件字节数可能会发生变化.
如果你不想ftp修改原文件, 可以使用bin模式(二进制模式)传输文本。
文件读写
打开文件
您可以使用 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 中的常量。
C 标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。
写入文件
下面是把字符写入到流中的最简单的函数:
int fputc( int c, FILE *fp );
|
函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。您可以使用下面的函数来把一个以 null 结尾的字符串写入到流中:
int fputs( const char *s, FILE *fp );
|
函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。
您也可以使用 int fprintf(FILE *fp,const char *format, …) 函数来写把一个字符串写入到文件中。尝试下面的实例:
注意:请确保您有可用的 tmp 目录,如果不存在该目录,则需要在您的计算机上先创建该目录。
/tmp 一般是 Linux 系统上的临时目录,如果你在 Windows 系统上运行,则需要修改为本地环境中已存在的目录,例如:C:tmp、D:tmp等。
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); } |
当上面的代码被编译和执行时,它会在 /tmp 目录中创建一个新的文件 test.txt,并使用两个不同的函数写入两行。接下来让我们来读取这个文件。
读取文件
下面是从文件读取单个字符的最简单的函数:
int fgetc( FILE * fp );
|
fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。下面的函数允许您从流中读取一个字符串:
char *fgets( char *buf, int n, FILE *fp );
|
函数 fgets() 从 fp 所指向的输入流中读取 n – 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。
如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。
您也可以使用 int fscanf(FILE *fp, const char *format, …) 函数来从文件中读取字符串,但是在遇到第一个空格字符时,它会停止读取。
int main() { FILE *fp = NULL; char buff[255]; fp = fopen("/tmp/test.txt", "r"); fscanf(fp, "%s", buff); printf("1: %sn", buff ); fgets(buff, 255, (FILE*)fp); printf("2: %sn", buff ); fgets(buff, 255, (FILE*)fp); printf("3: %sn", buff ); fclose(fp); } |
当上面的代码被编译和执行时,它会读取上一部分创建的文件,产生下列结果:
1: This 2: is testing for fprintf... 3: This is testing for fputs... |
首先,fscanf() 方法只读取了 This,因为它在后边遇到了一个空格。其次,调用 fgets() 读取剩余的部分,直到行尾。最后,调用 fgets() 完整地读取第二行。
移动文件指针
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)。
int main(){ FILE *fp = NULL; fp = fopen("test.txt", "r+"); // 确保 test.txt 文件已创建 fprintf(fp, "This is testing for fprintf...n"); fseek(fp, 10, SEEK_SET); if (fputc(65,fp) == EOF) { printf("fputc fail"); } fclose(fp); } |
执行结束后,打开 test.txt 文件:
This is teAting for fprintf...
|
注意: 只有用 r+ 模式打开文件才能插入内容,w 或 w+ 模式都会清空掉原来文件的内容再来写,a 或 a+ 模式即总会在文件最尾添加内容,哪怕用 fseek() 移动了文件指针位置。
二进制 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); |
这两个函数都是用于存储块的读写 – 通常是数组或结构体。
预处理
C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
预处理指令
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
|
这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 替换为 20。使用 #define 定义常量来增强可读性。
#include <stdio.h> #include "myheader.h" |
这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。
#undef FILE_SIZE #define FILE_SIZE 42 |
这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。
#ifndef MESSAGE #define MESSAGE "You wish!" #endif |
这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。
#ifdef DEBUG /* Your debugging statements here */ #endif |
这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。
在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。
预定义宏
宏 | 描述 |
---|---|
DATE | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
TIME | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
FILE | 这会包含当前文件名,一个字符串常量。 |
LINE | 这会包含当前行号,一个十进制常量。 |
STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
main() { printf("File :%sn", __FILE__ ); printf("Date :%sn", __DATE__ ); printf("Time :%sn", __TIME__ ); printf("Line :%dn", __LINE__ ); printf("ANSI :%dn", __STDC__ ); } |
当上面的代码(在文件 test.c 中)被编译和执行时,它会产生下列结果:
File :test.c Date :Jun 2 2012 Time :03:36:24 Line :8 ANSI :1 |
预处理运算符
C 预处理器提供了下列的运算符来帮助您创建宏:
宏延续运算符()
一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符()。例如:
#define message_for(a, b) printf(#a " and " #b ": We love you!n") |
字符串常量化运算符(#)
在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:
printf(#a " and " #b ": We love you!n") int main(void) { message_for(Carole, Debra); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Carole and Debra: We love you!
|
对于字符常量化宏一例中,给 printf 句加上 {} 及 ;,则主函数中的 message_for(Carole, Debra) 可以省略 ;:
{printf(#a " and " #b ": We love you!n");} int main(void) { message_for(Carole, Debra) return 0; } |
标记粘贴运算符(##)
宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:
#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);
|
这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。
defined() 运算符
预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:
int main(void) { printf("Here is the message: %sn", MESSAGE); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Here is the message: You wish!
|
参数化的宏
CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:
int square(int x) { return x * x; } |
我们可以使用宏重写上面的代码,如下:
#define square(x) ((x) * (x))
|
在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:
int main(void) { printf("Max between 20 and 10 is %dn", MAX(10, 20)); return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
Max between 20 and 10 is 20
|
使用#define含参时,参数括号很重要,如上例中省略括号会导致运算错误:
int main(void) { printf("square 5+4 is %dn", square(5+4)); printf("square_1 5+4 is %dn", square_1(5+4)); return 0; } |
输出结果为:
square 5+4 is 81 square_1 5+4 is 29 |
原因:
square 等价于 (5+4)*(5+4)=81 square_1 等价于 5+4*5+4=29 |
用#define宏定义将a,b交换,不使用中间变量,两种方法实现swap(x,y);
int main() { int a,b; scanf("%d %d",&a,&b); printf("Max number is:%dn",MAX(a,b)); printf("交换前:x=%d,y=%dn",a,b); SWAP1(a,b); printf("交换后:x=%d,y=%dn",a,b); SWAP2(a,b); printf("再次交换后:x=%d,y=%dn",a,b); return 0; } |
头文件
建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
|
只引用一次头文件:
the entire header file file |
这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
条件引用:
有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:
#if SYSTEM_1 # include "system_1.h" #elif SYSTEM_2 # include "system_2.h" #elif SYSTEM_3 ... #endif |
但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,您只需要使用宏名称代替即可:
#define SYSTEM_H "system_1.h" ... #include SYSTEM_H |
SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。
在有多个 .h 文件和多个 .c 文件的时候,往往我们会用一个 global.h 的头文件来包括所有的 .h 文件,然后在除 global.h 文件外的头文件中 包含 global.h 就可以实现所有头文件的包含,同时不会乱。方便在各个文件里面调用其他文件的函数或者变量。
#ifndef _GLOBAL_H #define _GLOBAL_H #include <fstream> #include <iostream> #include <math.h> #include <Config.h> |
库函数
string,h
序号 | 函数 | 描述 |
---|---|---|
1 | void *memchr(const void *str, int c, size_t n) | 在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。 |
2 | int memcmp(const void *str1, const void *str2, size_t n) | 把 str1 和 str2 的前 n 个字节进行比较。 |
3 | void *memcpy(void *dest, const void *src, size_t n) | 从 src 复制 n 个字符到 dest。 |
4 | void *memmove(void *dest, const void *src, size_t n) | 另一个用于从 src 复制 n 个字符到 dest 的函数。 |
5 | void *memset(void *str, int c, size_t n) | 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。 |
6 | char *strcat(char *dest, const char *src) | 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。 |
7 | char *strncat(char *dest, const char *src, size_t n) | 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。 |
8 | char *strchr(const char *str, int c) | 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。 |
9 | int strcmp(const char *str1, const char *str2) | 把 str1 所指向的字符串和 str2 所指向的字符串进行比较。 |
错误处理
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。
C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息。
- perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
- strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
让我们来模拟一种错误情况,尝试打开一个不存在的文件。您可以使用多种方式来输出错误消息,在这里我们使用函数来演示用法。另外有一点需要注意,您应该使用 stderr 文件流来输出所有的错误。
extern int errno ; int main () { FILE * pf; int errnum; pf = fopen ("unexist.txt", "rb"); if (pf == NULL) { errnum = errno; fprintf(stderr, "错误号: %dn", errno); perror("通过 perror 输出错误"); fprintf(stderr, "打开文件错误: %sn", strerror( errnum )); } else { fclose (pf); } return 0; } |
当上面的代码被编译和执行时,它会产生下列结果:
错误号: 2 通过 perror 输出错误: No such file or directory 打开文件错误: No such file or directory |
在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误。
为了避免这种情况发生,下面的代码在进行除法运算前会先检查除数是否为零:
main() { int dividend = 20; int divisor = 0; int quotient; if( divisor == 0){ fprintf(stderr, "除数为 0 退出运行...n"); exit(-1); } quotient = dividend / divisor; fprintf(stderr, "quotient 变量的值为 : %dn", quotient ); exit(0); } |
当上面的代码被编译和执行时,它会产生下列结果:
除数为 0 退出运行...
|
通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。
如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。所以,上面的程序可以写成:
main() { int dividend = 20; int divisor = 5; int quotient; if( divisor == 0){ fprintf(stderr, "除数为 0 退出运行...n"); exit(EXIT_FAILURE); } quotient = dividend / divisor; fprintf(stderr, "quotient 变量的值为: %dn", quotient ); exit(EXIT_SUCCESS); } |
当上面的代码被编译和执行时,它会产生下列结果:
quotient 变量的值为 : 4
|
可变参数
有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。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 变量的内存。
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(可变参数) 的意思。
现在让我们按照上面的步骤,来编写一个带有可变数量参数的函数,并返回它们的平均值:
/* ANSI标准形式的声明方式,括号内的省略号表示可选参数 */ 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"); } |
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1) 首先在函数里定义一个 va_list 型的变量,这里是 arg_ptr,这个变量是指向参数的指针。
2) 然后用 va_start 宏初始化变量 arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。
3) 然后用 va_arg 返回可变的参数,并赋值给整数 j。va_arg 的第二个参数是你要返回的参数的类型,这里是int型。
4) 最后用 va_end 宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用 va_arg 获取各个参数。
可变参数的工作原理,以32位机为例:
- 1.函数参数的传递存储在栈中,从右至左压入栈中,压栈过程为递减。
- 2.参数的传递以4字节对齐,float/double这里不讨论。
void debug_arg(unsigned int num, ...) { unsigned int i = 0; unsigned int *addr = # for (i = 0; i <= num; i++) { /* *(addr + i) 从左往右依次取出传递进来的参数,类似于出栈过程 */ printf("i=%d,value=%drn", i, *(addr + i)); } } int main(void) { debug_arg(3, 66, 88, 666); return 0; } |
// 64 位机器用 8 字节对齐, 32 位 4 位对齐 //VA_LIST套宏中可以使用,用来改变INTSIZEOF中t的类型 //固定参数详见 void test(int a, double b, char* c) { char *p = (char*)&a; //因为&a = void 类型 需要转换,void * =&a 不需要转换但是使用时要转换 printf("%p %p %pn", &a, &b, &c); //观察地址变化 printf("%p %s",(p+8),*(char**)(p+8+8));//64位机器时加8内存大小8字节对齐 return; } //可变参数实验 void test1(char* s,char *st,...) { char *ppt =(char*)&s; //printf("%p %p %p %p,",ppt,&s,&st,(char*)ppt+8); printf("%p %p %p %pn", ppt, &s, &st, ppt + 8); printf("%sn", *(char**)(ppt+4)); printf(" %dn",*(int*)(ppt + 4+4));//当是X64就加8 X86就加4因为内存对齐规则 return; } int main() { char *p = "Hello world"; test1("111","eee",45234,23); //test(2, 2.2, "Hello world");x void *s = &p; printf("%s", *(char**)s); return 0; } |
内存管理
序号 | 函数和描述 | 描述 |
---|---|---|
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 * 类型可以通过类型转换强制转换为任何其它类型的指针。
动态内存分配
编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:
char name[100];
|
但是,如果您预先不知道需要存储的文本长度,例如您向存储有关一个主题的详细描述。
在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:
int main() { char name[100]; char *description; strcpy(name, "Zara Ali"); /* 动态分配内存 */ description = (char *)malloc( 200 * sizeof(char) ); if( description == NULL ) { fprintf(stderr, "Error - unable to allocate required memoryn"); } else { strcpy( description, "Zara ali a DPS student in class 10th"); } printf("Name = %sn", name ); printf("Description: %sn", description ); } |
当上面的代码被编译和执行时,它会产生下列结果:
Name = Zara Ali Description: Zara ali a DPS student in class 10th |
上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:
calloc(200, sizeof(char));
|
当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。
重新调整内存的大小和释放内存
当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。
或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。让我们使用 realloc() 和 free() 函数,再次查看上面的实例:
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 memoryn"); } 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 memoryn"); } else { strcat( description, "She is in class 10th"); } printf("Name = %sn", name ); printf("Description: %sn", description ); /* 使用 free() 函数释放内存 */ free(description); } |
当上面的代码被编译和执行时,它会产生下列结果:
Name = Zara Ali Description: Zara ali a DPS student.She is in class 10th |
您可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。
void指针
对于 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:正确 |
对于我们手动分配的内存,在 C 语言中是不用强制转换类型的。
description = malloc( 200 * sizeof(char) ); // C 语言正确。 description = malloc( 200 * sizeof(char) ); // C++ 错误 |
但是 C++ 是强制要求的,不然会报错。
动态可变长的结构体
动态可变长的结构体:
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"); |
注意:一个结构体中只能有一个可变长的成员,并且该成员必须是最后一个成员。
命令行参数
执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。
命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。
下面是一个简单的实例,检查命令行是否有提供参数,并根据参数执行相应的动作:
int main( int argc, char *argv[] ) { if( argc == 2 ) { printf("The argument supplied is %sn", argv[1]); } else if( argc > 2 ) { printf("Too many arguments supplied.n"); } else { printf("One argument expected.n"); } } |
使用一个参数,编译并执行上面的代码,它会产生下列结果:
$./a.out testing The argument supplied is testing |
使用两个参数,编译并执行上面的代码,它会产生下列结果:
$./a.out testing1 testing2 Too many arguments supplied. |
不传任何参数,编译并执行上面的代码,它会产生下列结果:
$./a.out One argument expected |
应当指出的是,argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针,argv[n] 是最后一个参数。如果没有提供任何参数,argc 将为 1,否则,如果传递了一个参数,*argc** 将被设置为 2。
多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号 “” 或单引号 ‘’ 内部。
让我们重新编写上面的实例,有一个空间,那么你可以通过这样的观点,把它们放在双引号或单引号””””。让我们重新编写上面的实例,向程序传递一个放置在双引号内部的命令行参数:
int main( int argc, char *argv[] ) { printf("Program name %sn", argv[0]); if( argc == 2 ) { printf("The argument supplied is %sn", 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 |
需要注意的是:
main 的两个参数的参数名如下:
> int main( int argc, char *argv[] ) >
并不一定这样写,只是约定俗成罢了。但是亦可以写成下面这样:
> int main( int test_argc, char *test_argv[] ) >
任意你喜欢的名字。
但是大部分人还是写成开头那样的,如下:
> int main( int argc, char *argv[] ) >
顺序表
定义
// 顺序表的结构定义 typedef struct { int data[Maxsize]; // 存放数组的数组 int length; // 顺序表的实际长度 }SeqList; // 顺序表类型名为SeqList |
插入
// 顺序表插入运算 void InsertSeqlist(SeqList *L, int x, int i) { int j; if(L->length == Maxsize) printf("表已满"); if(i < 1 || i > L->length + 1) printf("位置错"); // 检查插入位置是否合法 for(j = L->length;j >= i;j--) { L->data[j] = L->data[j - 1]; // 整体依次向后移动 } L->data[i - 1] = x; L->length++; } main() { int i; int n = 5; printf("n"); SeqList s = {{1,2,3,4,5}, n}; InsertSeqlist(&s, 99, 2); for(i = 0;i < n + 1;i++) { printf("%dn", s.data[i]); } } |
删除
// 删除线性表中第i个数据结点 void DeleteSeqList(SeqList *L, int i) { int j; if(i < 1 || i > L->length) printf("非法位置n"); for(j = i;j < L->length;j++) { L->data[j - 1] = L->data[j]; // 依次左移 } L->length--; } main() { int i; int n = 5; SeqList L = {{1,2,3,4,5}, n}; DeleteSeqList(&L, 2); for(i = 0;i < n - 1;i++) { printf("%dn", L.data[i]); } } |
查找
// 在顺序表中查找值为x的结点 int LocateSeqList(SeqList L, int x) { int i = 0; while(i < L.length && L.data[i] != x) i++; if(i < L.length) return i + 1; else return 0; } main() { SeqList L = {{100, 58, 102, 99, 2}, 5}; printf("%dn", LocateSeqList(L, 2)); // 5 } |
链表
链表是一种常见的数据结构——与数组不同的是:
- 数组首先需要在定义时声明数组大小,如果像这个数组中加入的元素个数超过了数组的长度时,便不能正确保存所有内容;链表可以根据大小需要进行拓展。
- 其次数组是同一数据类型的元素集合,在内存中是按一定顺序连续排列存放的;链表常用malloc等函数动态随机分配空间,用指针相连。
在链表中,每一个元素包含两个部分;数据部分和指针部分。数据部分用来存放元素所包含的数据,指针部分用来指向下一个元素。最后一个元素的指针指向NULL,表示指向的地址为空。整体用结构体来定义,指针部分定义为指向本结构体类型的指针类型。
静态链表需要数组来实现,即把线性表的元素存放在数组中。数组单元存放链表结点,结点的链域指向下一个元素的位置,即下一个元素所在数组单元的下标。这些元素可能在物理上是连续存放的,也有可能是不连续的,它们之间通过逻辑关系来连接——这就要涉及到数组长度定义的问题,实现无法预知定义多大的数组,动态链表随即出现。
动态链表指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点的数据,并建立起前后相连的关系。
单链表
单链表中,每个结点只有一个指针,所有结点都是单线联系,除了末为结点指针为空外,每个结点的指针都指向下一个结点,一环一环形成一条线性链。
创建并遍历
/*单向链表*/ struct Student/*建立学生信息结构体模型*/ { char cName[20];/*学生姓名*/ int iNumber;/*学生学号*/ struct student *next;/*指向本结构体类型的指针类型*/ }; int iCount;/*全局变量表示链表长度*/ struct Student *Create();/*创建链表函数声明*/ void print(struct Student *);/*遍历输出链表函数声明*/ int main() { int insert_n=2;/*定义并初始化要插入的结点号*/ int delete_n=2;/*定义并初始化要删除的结点号*/ struct Student *pHead;/*声明一个指向学生信息结构体的指针作pHead为头结点传递*/ pHead=Create();/*创建链表,返回链表的头指针给pHead*/ print(pHead);/*将指针pHead传入输出函数遍历输出*/ return 0; } struct Student *Create() { struct Student *pHead=NULL;/*初始化链表,头指针为空*/ struct Student *pEnd,*pNew; iCount=0;/*初始化链表长度*/ pEnd=pNew=(struct Student *)malloc(sizeof(struct Student));/*动态开辟一个学生信息结构体类型大小的空间,使得pEnd和pNew同时指向该结构体空间*/ scanf("%s",pNew->cName);/*从输入流获取第一个学生姓名*/ scanf("%d",&pNew->iNumber);/*从输入流获取第一个学生学号*/ while(pNew->iNumber!=0)/*设定循环结束条件——学号不为0时*/ { iCount++;/*链表长度+1,即学生信息个数+1*/ if(iCount==1)/*如果链表长度刚刚加为1,执行*/ { pNew->next=pHead;/*使指针指向为空*/ pEnd=pNew;/*跟踪新加入的结点*/ pHead=pNew;/*头结点指向首结点*/ } else/*如果链表已经建立,长度大于等于2时,执行*/ { pNew->next=NULL;/*新结点的指针为空*/ pEnd->next=pNew;/*原来的结点指向新结点*/ pEnd=pNew;/*pEnd指向新结点*/ } pNew=(struct Student *)malloc(sizeof(struct Student));/*再次分配结点的内存空间*/ scanf("%s",pNew->cName);/*从输入流获取第一个学生姓名*/ scanf("%d",&pNew->iNumber);/*从输入流获取第一个学生学号*/ } free(pNew);/*释放结点空间*/ return pHead;/*返回创建出的头指针*/ } void print(struct Student *pHead) { struct Student *pTemp;/*定义指向一个学生信息结构体类型的临时指针*/ int iIndex=1;/*定义并出事哈变量iIndex,用来标识第几个学生(信息)*/ printf("总共%d个学生(信息):n",iCount); pTemp=pHead;/*指针得到首结点的地址*/ while(pTemp!=NULL)/*当临时指针不指向NULL时*/ { printf("第%d个学生信息:n",iIndex); printf("姓名:%s",pTemp->cName); /*输出姓名*/ printf("学号:%d",pTemp->iNumber);/*输出学号*/ pTemp=pTemp->next;/*移动临时指针到下一个结点*/ iIndex++;/*进行自加运算*/ } } |
插入与删除
int main() { int insert_n=2;/*定义并初始化要插入的结点号*/ int delete_n=2;/*定义并初始化要删除的结点号*/ struct Student *pHead;/*声明一个指向学生信息结构体的指针作pHead为头结点传递*/ pHead=Create();/*创建链表,返回链表的头指针给pHead*/ pHead=Insert(pHead,insert_n);/*将指针pHead和要插入的结点数传递给插入函数*/ print(pHead);/*将指针pHead传入输出函数遍历输出*/ Delete(pHead,delete_n);/*将指针pHead和要删除的结点数传递给删除函数*/ print(pHead);/*将指针pHead传入输出函数遍历输出*/ return 0; } |
struct Student *Insert(struct Student *pHead,int number) { struct Student *p=pHead,*pNew;/*定义pNew指向新分配的空间*/ while(p&&p->iNumber!=number) p=p->next;/*使临时结点跟踪到要插入的位置(该实例必须存在学号为number的信息,插入其后,否则出错)*/ printf("姓名和学号:n"); /*分配内存空间,返回该内存空间的地址*/ pNew=(struct Student *)malloc(sizeof(struct Student)); scanf("%s",pNew->cName); scanf("%d",&pNew->iNumber); pNew->next=p->next;/*新结点指针指向原来的结点*/ p->next=pNew;/*头指针指向新结点*/ iCount++;/*增加链表结点数量*/ return pHead;/*返回头指针*/ } |
void Delete(struct Student *pHead,int number) { int i; struct Student *pTemp;/*临时指针*/ struct Student *pPre;/*表示要删除结点前的结点*/ pTemp=pHead;/*得到链表的头结点*/ pPre=pTemp; for(i=0;i<number;i++) {/*通过for循环使得Temp指向要删除的结点*/ pPre=pTemp; pTemp=pTemp->next; } pPre->next=pTemp->next;/*连接删除结点两边的结点*/ free(pTemp);/*释放要删除结点的内存空间*/ iCount--;/*减少链表中的结点个数*/ } |
双向链表
双向链表基于单链表。单链表是单向的,有一个头结点,一个尾结点,要访问任何结点,都必须知道头结点,不能逆着进行。而双链表添加了一个指针域,通过两个指针域,分别指向结点的前结点和后结点。这样的话,可以通过双链表的任何结点,访问到它的前结点和后结点。
在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点的地址,一般称为右链域;一个存储直接前驱结点地址,一般称之为左链域。
创建与遍历
typedef struct Node { char name[20]; struct Node *llink,*rlink; }STUD; STUD *creat(int);void print(STUD *); int main() { int number; char studname[20]; STUD *head,*searchpoint; number=N; head=creat(number); print(head); printf("请输入你要查找的人的姓名:"); scanf("%s",studname); searchpoint=search(head,studname); printf("你所要查找的人的姓名是:%s",*&searchpoint->name); return 0; } STUD *creat(int n) { STUD *p,*h,*s; int i; if((h=(STUD *)malloc(sizeof(STUD)))==NULL) { printf("不能分配内存空间"); exit(0); } h->name[0]=''; h->llink=NULL; h->rlink=NULL; p=h; for(i=0;i<n;i++) { if((s=(STUD *)malloc(sizeof(STUD)))==NULL) { printf("不能分配内存空间"); exit(0); } p->rlink=s; printf("请输入第%d个人的姓名",i+1); scanf("%s",s->name); s->llink=p; s->rlink=NULL; p=s; } h->llink=s; p->rlink=h; return(h); } void print(STUD *h) { int n; STUD *p; p=h->rlink; printf("数据信息为:n"); while(p!=h) { printf("%s ",&*(p->name)); p=p->rlink; } printf("n"); } |
查找
STUD *search(STUD *h,char *x) { STUD *p; char *y; p=h->rlink; while(p!=h) { y=p->name; if(strcmp(y,x)==0) return(p); else p=p->rlink; } printf("没有查到该数据!"); } |
循环链表
单链表的最后一个结点的指针指向NULL,而循环链表的最后一个结点的指针指向链表头结点。
这样头尾相连,形成了一个环形的数据链。
循环链表的建立不需要专门的头结点。
判断循环链表是否为尾结点时,只需判断该节点的指针域是否指向链表头节点。
队列
队列,简称队,它是一种操作受限的线性表,其限制在表的一端进行插入,另一端进行删除。可进行插入的一端称为队尾(rear),可进行删除的一端称为队头(front)。
顺序循环队列
定义
typedef struct Squeue{ int data[Maxsize]; int front; int rear; }Squeue; |
队空状态 :qu.rear == qu.front
队满状态 : (qu.rear + 1) % Maxsize == qu.front
元素的进队操作 :qu.rear = (qu.rear + 1) % Maxsize ; qu.data[qu.rear] = x ;l
初始化
void InitQueue(Squeue &qu) { qu.front = qu.rear = 0; } |
判空
int isQueueEmpty(Squeue qu) { if(qu.front == qu.rear) { return 1; } else { return 0; } } |
入队操作
int inQueue(Squeue &qu,int x) { //若队满则无法入队 if((qu.rear + 1) % Maxsize == qu.front) { return 0; } qu.rear = (qu.rear + 1) % Maxsize; qu.data[qu.rear] = x; return 1; } |
出队操作
int deQueue(Squeue &qu,int &x) { //若队空则无法出队 if(qu.front == qu.rear) { return 0; } qu.front = (qu.front + 1) % Maxsize; x = qu.data[qu.front]; return 1; } |
队列长度
int length(Squene s){ return (s.rear-s.front+1) % MAXSIZE; } |
链表队列
定义
typedef struct QNode{ QElemType data; struct QNode *next; }Qnode,*Queneptr; typedef struct{ Queneptr front;//队头指针 Queneptr rear;//队尾指针 }LinkQuene; |
约定:
队空:s.front=s.rear(有头结点)
s.front=NULL(无头结点)
链队无队满的情况(假定内存足够大)
初始化
Initquene(LinkQuene &s){ s.front=(Queneptr *)malloc(sizeof(Qnode)); if(!s.front) exit(-1); s.rear=s.front; s.front->next=NULL; } |
判空
int isqueneempty(LinkQuene &s){ if(s.rear==NULL||s.front==NULL) return 1; return 0; } |
入队
void enquene(LinkQuene &s,QElemType e){ Queneptr p=(Queneptr *)malloc(sizeof(Qnode)); if(!p) exit(-1); p->data=e; p->next=NULL; s.rear->next=p; s.rear=p; } |
出队
void dequene(LinkQuene &s,QElemType &e){ if(s.rear==NULL||s.front==NULL){ printf("对空"); exit(-1); } Queneptr p=s.front->next; e=p->data; s.front->next=p->next; if(s.rear==p) s.rear=s.front; free(p); } |
销毁队列
int destory(LinkQuene &s){ while(s.front){ s.rear=s,front->next; free(s.front); s.front=s.rear; } if(s.rear==s.front) return 1; return 0; } |
原创文章,作者:小嵘源码,如若转载,请注明出处:https://www.lcpttec.com/c/