const关键字
在C和C++中,const
关键字用于修饰变量、函数、指针和类成员,以保护数据不被修改,提高代码的可读性和安全性
typedef关键字
C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE
typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:
BYTE b1, b2;
volatile关键字
使用volatile
关键字可以防止编译器对这些变量进行优化,确保每次访问时都直接从内存中读取变量的当前值,而不是使用可能已经存储在寄存器中的旧值。
static关键字
1、static修饰局部变量:
对其存储位置进行改变,存储在静态区;
改变其生命周期,为整个源程序,因此它只被初始化一次,并且被声明为静态的变量在这一函数被调用过程中维持其值不变。
2、static修饰全局变量:
改变其作用域,在模块内(但在函数体外),
一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问,它是一个本地的全局变量(只能被当前文件使用)。
3、static修饰函数:
改变其作用域,一个被声明为静态的函数只可被这一模块内的其它函数调用。
即,这个函数被限制在声明它的模块的本地范围内使用(只能被当前文件使用)
修饰局部变量实例:
#include <stdio.h>
void nonStaticVarFunction() {
// 没有使用static修饰的局部变量
int var = 0;
var++;
printf("Non-static var: %d\n", var); // 输出的值总是 1
}
void staticVarFunction() {
// 使用static修饰的局部变量
static int staticVar = 0;
staticVar++;
printf("Static var: %d\n", staticVar); // 输出的值会递增
}
int main() {
for (int i = 0; i < 3; i++) {
nonStaticVarFunction();
}
for (int i = 0; i < 3; i++) {
staticVarFunction();
}
return 0;
}
输出结果:
Non-static var: 1
Non-static var: 1
Non-static var: 1
Static var: 1
Static var: 2
Static var: 3
解析:这个例子展示了没有使用
static修饰的局部变量
var每次调用
nonStaticVarFunction函数时都会被重新初始化为0,然后增加1并打印出来,因此输出总是1。而
static修饰的局部变量
staticVar在第一次调用
staticVarFunction函数时初始化为0,之后每次调用函数都会保留上次调用结束时的值,并继续递增,所以输出是递增的。这说明了
static`修饰符对局部变量生命周期的影响,使变量跨函数调用保持状态。
修饰函数实例:假设我们有两个源文件:main.c
和help.c
。我们在help.c
中定义了一个static
函数,这意味着这个函数只能在help.c
中被调用,尝试在main.c
中调用它将导致链接错误。
#include <stdio.h>
// static修饰的函数,仅在本文件内可见
static void printHello() {
printf("Hello from helper.c\n");
}
// 全局函数,可以被其他文件调用
void callPrintHello() {
printHello();
}
输出结果:
#include <stdio.h>
// 函数声明
void callPrintHello();
int main() {
// 调用helper.c中定义的函数
callPrintHello(); // 正确,因为callPrintHello是全局的
// printHello(); // 错误,无法编译,因为printHello在main.c中不可见
return 0;
}
解析:在这个例子中,尽管main.c
尝试调用printHello
函数将失败,但我们可以通过调用callPrintHello
间接调用printHello
。这是因为printHello
函数是静态的,它的作用域被限制在help.c
文件内。这样的设计提高了程序的模块化。
内存管理
在 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 * 类型可以通过类型转换强制转换为任何其它类型的指针。
动态分配内存
编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:
char name[100];
但是,如果您预先不知道需要存储的文本长度,例如您想存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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 memory\n");
}
else
{
strcpy( description, "Zara ali a DPS student in class 10th");
}
printf("Name = %s\n", name );
printf("Description: %s\n", description );
}
当上面的代码被编译和执行时,它会产生下列结果:
Name = Zara Ali
Description: Zara ali a DPS student in class 10th
上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:
calloc(200, sizeof(char));
当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。
重新调整内存的大小和释放内存
当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。
或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。让我们使用 realloc() 和 free() 函数,再次查看上面的实例:
#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 时可用的内存不足。
C 语言中常用的内存管理函数和运算符
-
malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。
-
free() 函数:用于释放先前分配的内存。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态。
-
calloc() 函数:用于动态分配内存,并将其初始化为零。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。
-
realloc() 函数:用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。
-
sizeof 运算符:用于获取数据类型或变量的大小(以字节为单位)。
-
指针运算符:用于获取指针所指向的内存地址或变量的值。
-
& 运算符:用于获取变量的内存地址。
-
* 运算符:用于获取指针所指向的变量的值。
-
-> 运算符:用于指针访问结构体成员,语法为 pointer->member,等价于 (*pointer).member。
-
memcpy() 函数:用于从源内存区域复制数据到目标内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。
-
memmove() 函数:类似于 memcpy() 函数,但它可以处理重叠的内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。
运算符
算术运算符
下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
-- | 自减运算符,整数值减少 1 | A-- 将得到 9 |
实例
以下实例演示了 a++ 与 ++a 的区别:
#include <stdio.h>
int main()
{
int c;
int a = 10;
c = a++;
printf("先赋值后运算:\n");
printf("Line 1 - c 的值是 %d\n", c );
printf("Line 2 - a 的值是 %d\n", a );
a = 10;
c = a--;
printf("Line 3 - c 的值是 %d\n", c );
printf("Line 4 - a 的值是 %d\n", a );
printf("先运算后赋值:\n");
a = 10;
c = ++a;
printf("Line 5 - c 的值是 %d\n", c );
printf("Line 6 - a 的值是 %d\n", a );
a = 10;
c = --a;
printf("Line 7 - c 的值是 %d\n", c );
printf("Line 8 - a 的值是 %d\n", a );
}
以上程序执行输出结果为:
先赋值后运算:
Line 1 - c 的值是 10
Line 2 - a 的值是 11
Line 3 - c 的值是 10
Line 4 - a 的值是 9
先运算后赋值:
Line 5 - c 的值是 11
Line 6 - a 的值是 11
Line 7 - c 的值是 9
Line 8 - a 的值是 9
关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
运算符 | 描述 | 实例 |
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
位运算符
位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:
p | q | p&q | p|q | p^q |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
~A = 1100 0011
下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
& | 对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。 按位与操作,按二进制位进行"与"运算。运算规则: 0&0=0; | (A & B) 将得到 12,即为 0000 1100 |
| | 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。 按位或运算符,按二进制位进行"或"运算。运算规则: 0|0=0; 0|1=1; 1|0=1; 1|1=1; | (A | B) 将得到 61,即为 0011 1101 |
^ | 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。 异或运算符,按二进制位进行"异或"运算。运算规则: 0^0=0; 0^1=1; 1^0=1; 1^1=0; | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。 取反运算符,按二进制位进行"取反"运算。运算规则: ~1=-2; ~0=-1; | (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 将操作数的所有位向右移动指定的位数。右移n位相当于除以 2 的 n 次方。 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
实例
请看下面的实例,了解 C 语言中所有可用的位运算符:
#include <stdio.h>
int main()
{
unsigned int a = 60; /* 60 = 0011 1100 */
unsigned int b = 13; /* 13 = 0000 1101 */
int c = 0;
c = a & b; /* 12 = 0000 1100 */
printf("Line 1 - c 的值是 %d\n", c );
c = a | b; /* 61 = 0011 1101 */
printf("Line 2 - c 的值是 %d\n", c );
c = a ^ b; /* 49 = 0011 0001 */
printf("Line 3 - c 的值是 %d\n", c );
c = ~a; /*-61 = 1100 0011 */
printf("Line 4 - c 的值是 %d\n", c );
c = a << 2; /* 240 = 1111 0000 */
printf("Line 5 - c 的值是 %d\n", c );
c = a >> 2; /* 15 = 0000 1111 */
printf("Line 6 - c 的值是 %d\n", c );
}
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - c 的值是 12
Line 2 - c 的值是 61
Line 3 - c 的值是 49
Line 4 - c 的值是 -61
Line 5 - c 的值是 240
Line 6 - c 的值是 15
赋值运算符
下表列出了 C 语言支持的赋值运算符:
运算符 | 描述 | 实例 |
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | 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 |
实例
请看下面的实例,了解 C 语言中所有可用的赋值运算符:
#include <stdio.h>
int main()
{
int a = 21;
int c ;
c = a;
printf("Line 1 - = 运算符实例,c 的值 = %d\n", c );
c += a;
printf("Line 2 - += 运算符实例,c 的值 = %d\n", c );
c -= a;
printf("Line 3 - -= 运算符实例,c 的值 = %d\n", c );
c *= a;
printf("Line 4 - *= 运算符实例,c 的值 = %d\n", c );
c /= a;
printf("Line 5 - /= 运算符实例,c 的值 = %d\n", c );
c = 200;
c %= a;
printf("Line 6 - %%= 运算符实例,c 的值 = %d\n", c );
c <<= 2;
printf("Line 7 - <<= 运算符实例,c 的值 = %d\n", c );
c >>= 2;
printf("Line 8 - >>= 运算符实例,c 的值 = %d\n", c );
c &= 2;
printf("Line 9 - &= 运算符实例,c 的值 = %d\n", c );
c ^= 2;
printf("Line 10 - ^= 运算符实例,c 的值 = %d\n", c );
c |= 2;
printf("Line 11 - |= 运算符实例,c 的值 = %d\n", c );
}
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - = 运算符实例,c 的值 = 21
Line 2 - += 运算符实例,c 的值 = 42
Line 3 - -= 运算符实例,c 的值 = 21
Line 4 - *= 运算符实例,c 的值 = 441
Line 5 - /= 运算符实例,c 的值 = 21
Line 6 - %= 运算符实例,c 的值 = 11
Line 7 - <<= 运算符实例,c 的值 = 44
Line 8 - >>= 运算符实例,c 的值 = 11
Line 9 - &= 运算符实例,c 的值 = 2
Line 10 - ^= 运算符实例,c 的值 = 0
Line 11 - |= 运算符实例,c 的值 = 2
杂项运算符 ↦ sizeof & 三元
下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof 和 ? :。
运算符 | 描述 | 实例 |
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
实例
请看下面的实例,了解 C 语言中所有可用的杂项运算符:
#include <stdio.h>
int main()
{
int a = 4;
short b;
double c;
int* ptr;
/* sizeof 运算符实例 */
printf("Line 1 - 变量 a 的大小 = %lu\n", sizeof(a) );
printf("Line 2 - 变量 b 的大小 = %lu\n", sizeof(b) );
printf("Line 3 - 变量 c 的大小 = %lu\n", sizeof(c) );
/* & 和 * 运算符实例 */
ptr = &a; /* 'ptr' 现在包含 'a' 的地址 */
printf("a 的值是 %d\n", a);
printf("*ptr 是 %d\n", *ptr);
/* 三元运算符实例 */
a = 10;
b = (a == 1) ? 20: 30;
printf( "b 的值是 %d\n", b );
b = (a == 10) ? 20: 30;
printf( "b 的值是 %d\n", b );
}
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - 变量 a 的大小 = 4
Line 2 - 变量 b 的大小 = 2
Line 3 - 变量 c 的大小 = 8
a 的值是 4
*ptr 是 4
b 的值是 30
b 的值是 20
C 中的运算符优先级
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。
例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
类别 | 运算符 | 结合性 |
后缀 | () [] -> . ++ - - | 从左到右 |
一元 | + - ! ~ ++ - - (type)* & sizeof | |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | << >> | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与 AND | & | 从左到右 |
位异或 XOR | ^ | 从左到右 |
位或 OR | | | 从左到右 |
逻辑与 AND | && | 从左到右 |
逻辑或 OR | || | 从左到右 |
条件 | ?: | |
赋值 | = += -= *= /= %=>>= <<= &= ^= |= | |
逗号 | , | 从左到右 |
实例
请看下面的实例,了解 C 语言中运算符的优先级:
#include <stdio.h>
int main()
{
int a = 20;
int b = 10;
int c = 15;
int d = 5;
int e;
e = (a + b) * c / d; // ( 30 * 15 ) / 5
printf("(a + b) * c / d 的值是 %d\n", e );
e = ((a + b) * c) / d; // (30 * 15 ) / 5
printf("((a + b) * c) / d 的值是 %d\n" , e );
e = (a + b) * (c / d); // (30) * (15/5)
printf("(a + b) * (c / d) 的值是 %d\n", e );
e = a + (b * c) / d; // 20 + (150/5)
printf("a + (b * c) / d 的值是 %d\n" , e );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
(a + b) * c / d 的值是 90
((a + b) * c) / d 的值是 90
(a + b) * (c / d) 的值是 90
a + (b * c) / d 的值是 50
结构体
C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。
结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型、指针类型等。
结构用于表示一条记录,假设您想要跟踪图书馆中书本的动态,您可能需要跟踪每本书的下列属性:
- Title
- Author
- Subject
- Book ID
定义结构
结构体定义由关键字 struct 和结构体名组成,结构体名可以根据需要自行定义。
struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:
struct tag {
member-list
member-list
member-list
...
} variable-list ;
tag 是结构体标签。
member-list 是标准的变量定义,比如 int i; 或者 float f;,或者其他有效的变量定义。
variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方式:
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
在一般情况下,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;
在上面的声明中,第一个和第二声明被编译器当作两个完全不同的类型,即使他们的成员列表是一样的,如果令 t3=&s1,则是非法的。
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
//此结构体的声明包含了其他的结构体
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;
};
结构体变量的初始化
和其它类型变量一样,对结构体变量可以在定义时指定初始值。
#include <stdio.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};
int main()
{
printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}
执行输出结果为:
title : C 语言
author: RUNOOB
subject: 编程语言
book_id: 123456
访问结构成员
为了访问结构的成员,我们使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。您可以使用 struct 关键字来定义结构类型的变量。下面的实例演示了结构的用法:
#include <stdio.h>
#include <string.h>
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 : %s\n", Book1.title);
printf( "Book 1 author : %s\n", Book1.author);
printf( "Book 1 subject : %s\n", Book1.subject);
printf( "Book 1 book_id : %d\n", Book1.book_id);
/* 输出 Book2 信息 */
printf( "Book 2 title : %s\n", Book2.title);
printf( "Book 2 author : %s\n", Book2.author);
printf( "Book 2 subject : %s\n", Book2.subject);
printf( "Book 2 book_id : %d\n", Book2.book_id);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700
结构作为函数参数
您可以把结构作为函数参数,传参方式与其他类型的变量或指针类似。您可以使用上面实例中的方式来访问结构变量:
#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);
}
当上面的代码被编译和执行时,它会产生下列结果:
Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700
指向结构的指针
您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似,如下所示:
struct Books *struct_pointer;
现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示:
struct_pointer = &Book1;
为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:
struct_pointer->title;
让我们使用结构指针来重写上面的实例,这将有助于您理解结构指针的概念:
#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 的地址来输出 Book1 信息 */
printBook( &Book1 );
/* 通过传 Book2 的地址来输出 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);
}
当上面的代码被编译和执行时,它会产生下列结果:
Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700
结构体大小的计算
C 语言中,我们可以使用 sizeof 运算符来计算结构体的大小,sizeof 返回的是给定类型或变量的字节大小。
对于结构体,sizeof 将返回结构体的总字节数,包括所有成员变量的大小以及可能的填充字节。
以下实例演示了如何计算结构体的大小:
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person;
printf("结构体 Person 大小为: %zu 字节\n", sizeof(person));
return 0;
}
以上实例中,我们定义了一个名为 Person 的结构体,它包含了一个字符数组 name、一个整数 age 和一个浮点数 height。
在 main 函数中,我们声明了一个 Person 类型的变量 person,然后使用 sizeof 运算符来获取 person 结构体的大小。
最后,我们使用 printf 函数打印出结构体的大小,输出结果如下:
结构体 Person 大小为: 28 字节
注意,结构体的大小可能会受到编译器的优化和对齐规则的影响,编译器可能会在结构体中插入一些额外的填充字节以对齐结构体的成员变量,以提高内存访问效率。因此,结构体的实际大小可能会大于成员变量大小的总和,如果你需要确切地了解结构体的内存布局和对齐方式,可以使用 offsetof 宏和 __attribute__((packed)) 属性等进一步控制和查询结构体的大小和对齐方式。
结构体对齐规则
结构体大小是最大对齐数的整数倍
- 1.结构体大小是最大对齐数的整数倍
- 2.如果结构体内有成员长度大于处理器的位数,那么就以处理器的位数作为对齐单位
- 3.#pragma 可以编译器的默认对齐数
实例
#include <stdio.h>
#include <string.h>
struct test1{
int i; //int型占用字节大小为4
};
struct test2{
int i; //int的4比char的1大为最大对齐数所以以4为对齐数
//一个4不够用,2*4=8
char c;
};
struct test3{
int i; //同上,这里double的8最大,以8为对齐数
//int占用了4字节空间剩下的4对于double来说不够用
//所以int相当于浪费了4字节的空间,第二个8开始给double,第三个8给char。所以是24
//在int i; 和double d; 之间再初始化一个int或者char,结构体占用的空间不变
//因为int空下的4个字节对于int和char来说够用了
double d;
char c;
};
struct test4{
int i; //结构体test为16,太大了满足第二条,这里以8为对齐数
struct test{
double d1;
double d2;
}t;
};
#pragma pack(1) //定义对齐位数为1,所以大小为所有字节数相加
struct test5{
char c;
short a;
int i;
double o;
};
int main(void)
{
printf("%ld\n",sizeof(struct test1));//4
printf("%ld\n",sizeof(struct test2));//8
printf("%ld\n",sizeof(struct test3));//24
printf("%ld\n",sizeof(struct test4));//24
printf("%ld\n",sizeof(struct test5));//15
return 0;
}
小题目
答案
扩展
枚举
枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量,它可以让数据更简洁,更易读。
枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
定义一个枚举类型,需要使用 enum 关键字,后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。
枚举语法定义格式为:
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,以此类推。
可以在定义枚举类型时改变枚举元素的值:
enum season {spring, summer=3, autumn, winter};
没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5
枚举变量的定义
前面我们只是声明了枚举类型,接下来我们看看如何定义枚举变量。
我们可以通过以下三种方式来定义枚举变量
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 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。
不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。
以下实例使用 for 来遍历枚举的元素:
实例
#include <stdio.h>
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
以下枚举类型不连续,这种枚举无法遍历。
enum
{
ENUM_0,
ENUM_10 = 10,
ENUM_11
};
枚举在 switch 中的使用:
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum color { red=1, green, blue };
enum color favorite_color;
/* 用户输入数字来选择颜色 */
printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");
scanf("%u", &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
你喜欢的颜色是红色
将整数转换为枚举
以下实例将整数转换为枚举:
#include <stdio.h>
#include <stdlib.h>
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
共用体
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
定义共用体
为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:
union Data
{
int i;
float f;
char str[20];
} data;
现在,Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。
共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。下面的实例将显示上面的共用体占用的总内存大小:
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
printf( "Memory size occupied by data : %d\n", sizeof(data));
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Memory size occupied by data : 20
访问共用体成员
为了访问共用体的成员,我们使用成员访问运算符(.)。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。您可以使用 union 关键字来定义共用体类型的变量。下面的实例演示了共用体的用法:
#include <stdio.h>
#include <string.h>
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 : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
在这里,我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 10
data.f : 220.500000
data.str : C Programming
在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。
强制类型转换
强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:
请看下面的实例,使用强制类型转换运算符把一个整数变量除以另一个整数变量,得到一个浮点数:
#include <stdio.h>
int main()
{
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;
printf("Value of mean : %f\n", mean );
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of mean : 3.400000
这里要注意的是强制类型转换运算符的优先级大于除法,因此 sum 的值首先被转换为 double 型,然后除以 count,得到一个类型为 double 的值。
类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。在编程时,有需要类型转换的时候都用上强制类型转换运算符,是一种良好的编程习惯。
整数提升
整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。请看下面的实例,在 int 中添加一个字符:
#include <stdio.h>
int main()
{
int i = 17;
char c = 'c'; /* ascii 值是 99 */
int sum;
sum = i + c;
printf("Value of sum : %d\n", sum );
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of sum : 116
在这里,sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 'c' 的值转换为对应的 ascii 值。
常用的算术转换
常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:
常用的算术转换不适用于赋值运算符、逻辑运算符 && 和 ||。让我们看看下面的实例来理解这个概念:
#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语言中,结构体(struct)是一种用户自定义的数据类型,可以包含多个不同类型的数据成员。以下是“.”和“->”的区别:
“.”运算符用于访问结构体变量中的成员,表示结构体变量的成员变量或成员函数。
例如:struct Student { char name[20]; int age; }; struct Student s; s.age = 18; 表示访问结构体变量s中的age成员变量,并将其值设置为18。
“->”运算符用于访问结构体指针中的成员,表示结构体指针变量所指向的结构体变量的成员变量或成员函数。
例如:struct Student { char name[20]; int age; }; struct Student *p = &s; p->age = 18; 表示访问结构体指针变量p所指向的结构体变量s中的age成员变量,并将其值设置为18。 需要注意的是,使用“->”运算符时,指针变量必须先指向一个有效的结构体变量,否则会导致程序出错。此外,使用“.”和“->”运算符访问结构体成员时,成员的访问权限可以通过访问修饰符(public、private、protected)来控制。
指针
指针即指针变量,用于存放其他数据单元(变量/数组/结构体/函数等)的首地址。若指针存放了某个数据单元的首地址,则这个指针指向了这个数据单元,若指针存放的值是0,则这个指针为空指针定义一个指针变量
以下6点转自知乎 C 语言指针怎么理解? - 知乎 (zhihu.com)
第一,“指针”通常用于保存一个地址,这个地址的数据类型在定义指针变量时确定。
举个例子,做个比较:
int a; //定义一个变量a,用于保存一个int类型。
int * b; //定义一个指针变量b,用于保存一个地址,这个地址所保存的数据应该是int类型。
第二,是变量就应该可以赋值,指针变量也一样。但一般不会给指针直接赋值一个数值,而是将其他变量的地址赋值给指针,或者其他指针的值赋值给这个指针。
继续上面的例子:
b=&a; //把变量a的地址赋值给b。“&”操作是取变量的地址。
继续举例:
int * c; //我们又定义一个指针c
c=b; //将b的值赋值给c,上面已经知道b是指针,它的值是a的地址,那么现在c 的值和b一样,也是个a的地址。
第三,指针变量保存的值,也就是地址是可以变的。
举个数组初始化的例子:
int d[100];
int * e;
e=&d[0]; //e保存了数组d的第一个数据的地址
for (int i=0; i<100; i++){
*e = 0; //把该地址中的数据赋值0
e++; //地址累加一次,也就是数组中下一个数据的地址
}
指针和其他变量一样,可以运算(一般是加减),也可以重新赋值。
第四,指针有啥用。
比方说,我们有个函数,如下:
int add(int x){
return (x+1); //把输入的值加1并返回结果。
}
好了,应用的时候是这样的:
{
int a=1;
a=add(a); //add函数返回的是a+1
//现在 a等于2
}
很简单吧,就是把a都累加一次。
用指针怎么写:
void add(int *y){ //给入的是一个int指针,是一个地址。
*y = *y + 1; //* 是指引用 这个地址所保存的变量
//这条语句的意思就是,把这个地址里的值加1,然后放回这个地址。
}
把这个函数用起来:
{
int a=1;
add(&a); //把a的地址传到函数里
//add函数,就是把a的值加1,再放回到变量a里。
//现在a等于2
}
试想一下,如果我们要对一个数据结构里的数据进行处理,那是不是就该传入这个结构的地址,在函数中依靠指针来引用这个地址的数据结构,进行运算。
第五,若已有一个指针变量,可不可以用另外一个指针来保存这个变量的地址呢。
可以的。
一个变量保存另一个指针的地址,那它就是通常所说是“指针的指针”了。
通常,指针的指针多用做(或指的是)函数指针或数据结构中有指针的情况。初学者搞不清,就不要勉强了。
第六,空指针可怕吗?
我们知道,一个变量要赋值后才能用,指针也是一样。指针不赋值(不给定地址)就拿来用,是不是也要出错。这个就是空指针。一般把指针赋值为Null,就是表明这个指针是空的,不能用。所以程序中一定要经常判别指针不是Null才能用。
最后,初学C语言,最好把复合的语句拆开来看,比较容易理解。像 int *a=&b; 这种,拆成
int * a;
a=&b;
就好理解了。
下面是不同类型字符的字节数。
数据类型 | 指向该数据类型的指针 | ||
(unsigned)char | 1字节 | (unsigned) char * | x字节 |
(unsigned)short | 2字节 | (unsigned) short * | x字节 |
(unsigned)int | 4字节 | (unsigned)int * | x字节 |
(unsigned) long | 4字节 | (unsigned)long * | x字节 |
float | 4字节 | float * | x字节 |
double | 8字节 | double * | x字节 |
16位系统:x=2,32位系统:x=4,64位系统:x=8
指针操作
若已经定义
int a; //定义一个int型的数据
int *p; //定义一个指向int型数据的指针
则
操作方式 | 举例 | 解释 |
取地址 | p=&a; | 将数据a的首地址赋值给p |
取内容 | *p; | 取出指针指向的数据单元 |
加 | p++; | 使指针向下移动1个数据宽度 |
p=p+5; | 使指针向下移动5个数据宽度 | |
减 | p--; | 使指针向上移动1个数据宽度 |
p=p-5; | 使指针向上移动5个数据宽度 |
简单认识
#include <stdio.h>
int main(void)
{
int a=0x66;
int *p;
p=&a;
printf("%x\n",a);
printf("%x\n",p);
printf("%x\n",*p);
return 0;
}
p 是一个指针变量,作为指针,它的内容就是地址。
在p=&a; 语句中,将a的地址赋值给了p
通过printf函数将a的数值,p的数值也就是a的地址以及p指向的内容打印出来。
结果如下
数组和指针
数组是一些相同数据类型的变量组成的集合,其数组名即为指向该数据类型的指针。数组的定义等效于申请内存、定义指针和初始化
数组名就是一个指针变量,也就是说如果将数组名打印出来的话,那么结果就是这个数组的地址。
下图的实验就证明了这一点,打印数组的内容和对数组名取内容的结果一样。
由此说明 ,数组名就是一个指针变量。数组第一个元素的地址也是数组的首地址。
所以在函数的形参为数组的时候,形参为指针类型
字符指针与字符数组
字符数组方式
char word[] = "zack";
printf("%s", word);
字符指针方式
指针方式操作字符串和数组操作字符串类似,可以把定义的指针看做是字符数组的数组名。
在内存中存储大致如下,
//除了定义一个字符数组外,还可以直接定义一个字符指针存储字符串
char *sentence = "Do not go gentle into that good night!";
//此时可以做字符串的操作
//输出
printf("%s", sentence);
//通过下标取字符
printf("%c", sentence[0]);
//获取字符串长度,其中 strlen 是 string.h 库中的方法
printf("%d", strlen(sentence));
注:字符指针方式区别于字符数组方式,字符数组不能通过数组名自增操作,但是字符指针是指针,可以自增操作。自增自减少会实现什么效果大家可以自己尝试运行一下
下面做个小练习,利用字符指针将字符数组 sentence 中的内容复制到字符数组 word 中:
//定义字符数组 sentence 和 word,给 sentence 赋初值
char sentence[] = "Do not go gentle into that good night!", word[100];
//定义字符指针,指向 word
char *ch = word;
int i;
//循环赋值
for(i = 0; sentence[i] != '\0'; i++){
*(ch + i) = sentence[i];
}
//在当 i 等于 sentence 的长度(sentence 的长度不包含'\0')时,
//i 继续自增,此时判断 sentence[0] != '\0'不符合,跳出循环,则 i 比 sentence 长度大 1
*(ch + i) = '\0';
//输出字符串,因为 ch 指向 word,所以输出结果是一样的
printf("ch = %s, word = %s", ch, word);
注:指针变量必须初始化一个有效值才能使用
多级指针及指针数组
(1)多级指针
指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。我们先看看二级指针,它们关系如下:
其中 p 为一级指针,pp 为二级指针。二级指针定义形式如下:
数据类型 **二级指针名;
和指针变量的定义类似,由于*是右结合的,所以*pp 相当于*(*p)。在本次定义中,二级指针的变量名为 pp,而不是**p。多级指针的定义就是定义时使用多个“*”号。下面用一个小程序给大家举例
//定义普通变量和指针变量
int *pi, i = 10;
//定义二级指针变量
int **ppi;
//给指针变量赋初值
pi = &i;
//给二级指针变量赋初值
ppi = π
//我们可以直接用二级指针做普通指针的操作
//获取 i 的内容
printf("i = %d", **ppi);
//获取 i 的地址
printf("i 的地址为%d", *ppi);
注:在初始化二级指针 ppi 时,不能直接 ppi = &&i,因为&i 获取的是一个具体的数值,而具体数字是没有指针的。
(2)指针数组
指针变量和普通变量一样,也能组成数组,指针数组的具体定义如下:
数据类型 *数组名[指针数组长度];
下面举一个简单的例子熟悉指针数组:
//定义一个数组
int nums[5] = {2, 3, 4, 5, 2}, i;
//定义一个指针数组
int *p[5];
//定义一个二级指针
int **pp;
//循环给指针数组赋值
for(i = 0; i < 5; i++){
p[i] = &nums[i];
}
//将指针数组的首地址赋值给 pp,数组 p 的数组名作为 p 的首地址,也作为 p 中第一个元素的地址。
//数组存放的内容为普通变量,则数组名为变量的指针;数组存放的内容为指针,则数组名为指针的指针。
pp = p;
//利用二级指针 pp 输出数组元素
for(i = 0; i < 5; i++){
//pp == &p[0] == &&nums[0],nums[0] == *p[0] == **pp
printf("%d", **pp);
//指针变量+整数的操作,即移动指针至下一个单元
pp++;
}
指针与多维数组
讲多维数组是个麻烦的事,因为多维数组和二维数组没有本质的区别,但是复杂度倒是高了许多。这里我主要还是用二维数组来举例,但是还是会给大家分析多维数组和指针的关系。
多维数组的地址
先用一个简单的数组来举例:
int nums[2][2] = {
{1, 2},
{2, 3}
};
我们可以从两个维度来分析:
- 先是第一个维度,将数组当成一种数据类型 x,那么二维数组就可以当成一个元素为 x 的一维数组。
- 如上面的例子,将数组看成数据类型 x,那么 nums 就有两个元素。nums[0]和 nums[1]。
- 我们取 nums[0]分析。将 nums[0]看做一个整体,作为一个名称可以用 x1 替换。则 x1[0]就是 nums[0][0],其值为 1。
我们知道数组名即为数组首地址,上面的二维数组有两个维度。首先我们把按照上面 1 来理解,那么 nums 就是一个数组,则nums 就作为这个数组的首地址。第二个维度还是取 nums[0],我们把 nums[0]作为一个名称,其中有两个元素。我们可以尝试以下语句:
printf("%d", nums[0]);
此语句的输出结果为一个指针,在实验过后,发现就是 nums[0][0]的地址。即数组第一个元素的地址。
如果再多一个维度,我们可以把二维数组看做一种数据类型 y,而三维数组就是一个变量为 y 的一维数组。而数组的地址我们要先确定是在哪个维度,再将数组某些维度看成一个整体,作为名称,此名称就是该维度的地址(这里有些绕)。
例:
//假设已初始化,二维数组数据类型设为 x,一维数组数据类型设为 y
int nums[2][2][2];
//此数组首地址为该数组名称
printf("此数组首地址为%d", nums);
//此数组可以看做存储了两个 x 类型元素的一维数组,则 nums[0] = x1 的地址为
printf("第二个维度的首地址为%d", nums[0]);
//而 x1 可以看做存储了两个 y 类型元素的一维数组,则 y1 = x1[0] = nums[0][0]
printf("第三个维度的首地址为%d", nums[0][0]);
三维数组实际存储形式如下:
实际存储内容的为最内层维度,且为连续的。对于 a 来说,其个跨度为 4 个单元;对 a[0]来说,其跨度为 2 个单元;对 a[0][0]来说,跨度为一个单元。有上面还可以得出:
a == a[0] == a[0][0] == &a[0][0][0];
上面的等式只是数值上相等,性质不同。
多维数组的指针
在学习指针与数组的时候,我们可以如下表示一个数组:
int nums[5] = {2, 4, 5, 6, 7};
int *p = nums;
在前面讲指针数组时,所有指针数组元素都指向一个数字,那么我们现在可以尝试用指针数组的每个元素指向一个数组:
//定义一个二维数组
int nums[2][2] = {
{1, 2},
{2, 3}
};
//此时 nums[0]、和 nums[1]各为一个数组
int *p[2] = {nums[0], nums[1]};
//我们可以用指针数组 p 操作一个二维数组
//p 为数组 p 的首地址,p[0] = nums[0] = *p,**p = nums[0][0]
printf("nums[0][0] = %d", **p);
//指针 + 整数形式,p+1 移动到 nums 的地址,*(p +1) = nums[1],则**(p + 1) = nums[1][0]
printf("nums[1][0] = %d", **(p + 1));
//先*p = nums[0],再*p + 1 = &nums[0][1],最后获取内容*(*p + 1)即为 nums[0][1]
printf("nums[0][1] = %d", *(*p + 1));
这里可能不能理解为什么*p + 1 = &nums[0][1],而不是 nums[1]。*p 获得的是一个一维数组,而 int 数组 + 1 的跨度只有 4 个字节,也就是一个单元。前面 p 是一维数组的指针,其跨度为一个数组。所以*p + 1 = &nums[0][1],而 p + 1 = nums[1]。
赋值问题
在对指针取内容之前,一定要确保指针指在了合法的位置,否则将会导致程序出现不可预知的错误
同级指针之间才能相互赋值,跨级赋值将会导致编译器报错或警告
int指针和int不能直接赋值,假设定义了 int a; int *p; 那么 a=p; 这样不行。
只能说把a的地址给p或者把p指向的内容给a , p=&a或者a=*p;
传递参数
这种形式相当于另外开辟一段空间把变量的数值复制过去给子函数处理,所以不会影响到数组内容的数值。
使用指针传递大容量的参数,主函数和子函数使用的是同一套数据,避免了参数传递过程中的数据复制,提高了运行效率,减少了内存占用
因此在下面的代码中
#include <stdio.h>
#include <string.h>
int FindMax(int *buf,int count)
{
int i;
int max=buf[0];
for(i=1;i<count;i++)
{
if(buf[i] > max)
{
max = buf[i];
}
}
return max;
}
int main(void)
{
int a[]={1,2,334,655,767,2};
int max;
max = FindMax(a,sizeof(a)/sizeof(int));
printf("%d\n",max);
return 0;
}
在主函数中通过用指针的方式把数组传递给子函数,这样只需要额外开辟出一个指针的空间,避免了内存的浪费。
对于数组的大小可以用 sizeof(数组名) / sizeof(数组的数据类型) ;求得。
但是这样的话,如果在子函数中对数组内容进行修改,那么原数据将会被覆盖,因此有时候为了保险起见可以在子函数的定义前面加入const关键字
int FindMax(const int *buf,int count)
多返回值
使用指针传递输出参数,利用主函数和子函数使用同一套数据的特性,实现数据的返回,可实现多返回值函数的设计
#include <stdio.h>
#include <string.h>
void FindMax( int *buf,int size,int *count,int *max)
{
int i;
*max=buf[0];
for(i=1;i<size;i++)
{
if(buf[i] > *max)
{
*max = buf[i];
*count = 1;
}
else if(buf[i] == *max)
{
(*count) ++;
}
}
}
int main(void)
{
int a[]={1,2,3,4,5,5,6,7,7};
int max;
int count;
FindMax(a,sizeof(a)/sizeof(int),&count,&max);
printf("%d\n",max);
printf("%d\n",count);
return 0;
}
假如说遇到需要多个函数返回值的情况,可以使用指针来进行操作。把变量的地址给子函数,子函数对地址所指向的内容进行写入操作。
传递返回值
将模块内的公有部分返回,让主函数持有模块的“句柄”,便于程序对指定对象的操作
比如用指针函数返回某个全局变量的地址,来对这个地址进行读写操作。
例如
#include <stdio.h>
#include <string.h>
int hhh[]={1,2,3,4};
int *get_add(void)
{
return hhh;
}
int main(void)
{
int *p;
p=get_add();
printf("%d\n",p[0]);
printf("%d\n",p[1]);
printf("%d\n",p[2]);
printf("%d\n",p[3]);
return 0;
}
复杂数据传输
将复杂格式的数据转换为字节,方便通信与存储。
比如对于 float 型的变量 ,定义char*类型指针对float取地址当作数组把四个字节一位一位的发送,再用float*来对数据进行float型的解析,把数组用float解码成float型数据。
#include <stdio.h>
#include <string.h>
int main(void)
{
unsigned char j=0;
float a=12.345;
unsigned char data[4];
unsigned char *p;
p=(unsigned char*)&a;
for(int i=0;i<4;i++)
{
data[i]=*(p+j);
j++;
}
for(int i=0;i<4;i++){
printf("%x\n",data[i]);
}
float *st;
st= (float *)data;
printf("%6f",(float)*st);
return 0;
}
定义char*指针,指向 a 的地址,并利用循环将a的地址的四个字节依次取内容发送到data[4]数组中;再用float*型的指针对数组取地址,接着取内容进行float解码得到原数值。
我真是个天才。
指针相关概念
指针数组
指针数组是一个数组,其元素都是指针。例如,int *arr[10];
定义了一个包含10个整数指针的数组。
指针数组名表示该指针数组的起始地址。
数组指针
数组指针是一个指针,它指向一个数组的起始位置。例如,int (*p)[10];
定义了一个指针,该指针指向一个包含10个整数的数组。
指针函数
指针函数是一个函数,其返回值是指针。例如,int *fun(int x);
定义了一个函数,该函数接受一个整数参数并返回一个整数指针。
函数指针
函数指针是一个指针,它指向一个函数。例如,void (*pFunc)(int);
定义了一个指针,该指针可以指向一个接受整数参数并返回无类型(void)的函数。
常量指针
常量指针是一个指针,其指向的内容不可改变,但指针本身可以改变指向。例如,int const *p;
定义了一个常量指针,它指向一个整数,但不能通过该指针修改该整数的值。
指针常量
指针常量是一个指针本身不可改变的指针。例如,int * const p;
定义了一个指针常量,它指向一个整数,并且不能改变其指向的地址。
C语言常用函数
strstr
char *strstr(const char *haystack, const char *needle)
参数
- haystack -- 要被检索的 C 字符串。
- needle -- 在 haystack 字符串内要搜索的小字符串。
返回值
该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。
实例
#include <stdio.h>
#include <string.h>
int main()
{
const char haystack[20] = "RUNOOB";
const char needle[10] = "NOOB";
char *ret;
ret = strstr(haystack, needle);
printf("子字符串是: %s\n", ret);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
子字符串是: NOOB
sprintf
int sprintf(char *str, const char *format, ...);
str
是一个指向字符数组的指针,用于存储格式化后的字符串。
format
是一个字符串,包含了格式化指令和普通字符,指示如何将后续的可变数量的参数转换为字符串。...
表示可变数量的参数,它们的类型和数量取决于format
字符串中指定的格式。
sprintf
函数 会将转换后的字符串复制到 str
参数指向的缓冲区中,并在字符串末尾添加一个空字符 ('\0') 作为字符串的结束符。函数返回值为格式化后字符串的长度,不包括添加的结束符。如果转换过程中发生错误,则返回一个负值。
以下代码使用 sprintf
函数将整数、浮点数和字符串格式化为字符串并存储在 str
缓冲区中:
#include <stdio.h>
int main() {
char str[100];
int num = 100;
float pi = 3.14159;
sprintf(str, "数值: %d, pi的值: %.2f", num, pi);
printf("格式化字符串: %s\n", str);
return 0;
}
输出将会是:
格式化字符串: 数值: 100, pi的值: 3.14
strcpy
char *strcpy(char *dest, const char *src)
参数
- dest -- 指向用于存储复制内容的目标数组。
- src -- 要复制的字符串。
返回值
该函数返回一个指向最终的目标字符串 dest 的指针。
实例
#include <stdio.h>
#include <string.h>
int main()
{
char src[40];
char dest[100];
memset(dest, '\0', sizeof(dest));
strcpy(src, "This is runoob.com");
strcpy(dest, src);
printf("最终的目标字符串: %s\n", dest);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
最终的目标字符串: This is runoob.com
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[]="Sample string";
char str2[40];
char str3[40];
strcpy (str2,str1);
strcpy (str3,"copy successful");
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
让我们编译并运行上面的程序,这将产生以下结果:
str1: Sample string
str2: Sample string
str3: copy successful
memset
void *memset(void *str, int c, size_t n)
参数
- str -- 指向要填充的内存区域的指针。
- c -- 要设置的值,通常是一个无符号字符。
- n -- 要被设置为该值的字节数。
返回值
该值返回一个指向存储区 str 的指针。
注意事项
memset()
并不对指针ptr
指向的内存区域做边界检查,因此使用时需要确保ptr
指向的内存区域足够大,避免发生越界访问。memset()
的第二个参数value
通常是一个int
类型的值,但实际上只使用了该值的低8位。这意味着在范围0
到255
之外的其他值可能会产生未定义的行为。
num
参数表示要设置的字节数,通常是通过sizeof()
或其他手段计算得到的。
实例
#include <stdio.h>
#include <string.h>
int main ()
{
char str[50];
strcpy(str,"This is string.h library function");
puts(str);
memset(str,'$',7);
puts(str);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
This is string.h library function
$$$$$$$ string.h library function
#include <stdio.h>
#include <string.h> // 引入 string.h 头文件以使用 memset
int main() {
char buffer[10];
// 将 buffer 数组的前5个字节设置为字符 'A',并添加字符串终止符
memset(buffer, 'A', 5);
buffer[5] = '\0'; // 确保添加字符串终止符
printf("Buffer after memset: %s\n", buffer);
// 将 buffer 数组清零,使用 '\0' 替代 0
memset(buffer, '\0', sizeof(buffer)); // 使用'\0'确保一致性及可读性
printf("Buffer after memset: %s\n", buffer);
return 0;
}
让我们编译并运行上面的程序,这将产生以下结果:
Buffer after memset: AAAAA
Buffer after memset:
memcpy
void *memcpy(void *str1, const void *str2, size_t n)
参数
- str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
- str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
- n -- 要被复制的字节数。
返回值
该函数返回一个指向目标存储区 str1 的指针。
实例
复制 src 到 dest
// 将字符串复制到数组 dest 中
#include <stdio.h>
#include <string.h>
int main ()
{
const char src[50] = "http://www.runoob.com";
char dest[50];
memcpy(dest, src, strlen(src)+1);
printf("dest = %s\n", dest);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
dest = http://www.runoob.com
#include <stdio.h>
#include<string.h>
int main()
{
char *s="http://www.runoob.com";
char d[20];
memcpy(d, s+11, 6);// 从第 11 个字符(r)开始复制,连续复制 6 个字符(runoob)
// 或者 memcpy(d, s+11*sizeof(char), 6*sizeof(char));
d[6]='\0';
printf("%s", d);
return 0;
}
让我们编译并运行上面的程序,这将产生以下结果:
runoob
覆盖原有部分数据:
#include<stdio.h>
#include<string.h>
int main(void)
{
char src[] = "***";
char dest[] = "abcdefg";
printf("使用 memcpy 前: %s\n", dest);
memcpy(dest, src, strlen(src));
printf("使用 memcpy 后: %s\n", dest);
return 0;
}
让我们编译并运行上面的程序,这将产生以下结果:
使用 memcpy 前: abcdefg
使用 memcpy 后: ***defg
malloc
void *malloc(size_t size)
参数
- size -- 内存块的大小,以字节为单位。
返回值
该函数返回一个指针 ,指向已分配大小的内存。如果请求失败,则返回 NULL。
实例
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char *str;
/* 最初的内存分配 */
str = (char *) malloc(15);
strcpy(str, "runoob");
printf("String = %s, Address = %u\n", str, str);
/* 重新分配内存 */
str = (char *) realloc(str, 25);
strcat(str, ".com");
printf("String = %s, Address = %u\n", str, str);
free(str);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
String = runoob, Address = 3662685808
String = runoob.com, Address = 3662685808
free
void free(void *ptr)
参数
- ptr -- 指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。
返回值
该函数不返回任何值。
实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *str;
/* 最初的内存分配 */
str = (char *) malloc(15);
strcpy(str, "runoob");
printf("String = %s, Address = %p\n", str, str);
/* 重新分配内存 */
str = (char *) realloc(str, 25);
strcat(str, ".com");
printf("String = %s, Address = %p\n", str, str);
/* 释放已分配的内存 */
free(str);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
String = runoob, Address = 0x7fe4e4c02b10
String = runoob.com, Address = 0x7fe4e4c02b10
注意事项
-
释放正确的内存:只能释放通过动态内存分配函数分配的内存,不能释放由其他方式分配的内存(例如局部变量或全局变量)。
-
避免重复释放:同一个内存块不能多次释放,否则可能导致未定义行为。
释放后指针处理:释放内存后,指针仍然指向已释放的内存位置。为了避免悬空指针,可以将指针设为 NULL
。
free(array);
array = NULL;
检查空指针:调用 free()
前最好检查指针是否为空,以确保程序稳定性。
if (array != NULL) {
free(array);
array = NULL;
}
内存泄漏与悬空指针
- 内存泄漏:如果动态分配的内存没有被释放或丢失了对其的引用,内存将无法再被程序使用,造成内存泄漏。
- 悬空指针:指向已释放内存的指针称为悬空指针。如果悬空指针被再次访问,会导致未定义行为,可能引起程序崩溃或数据损坏。
在 C 语言中,调用 free() 释放动态分配的内存后,该指针仍然保存着之前分配的地址,这会导致悬空指针(dangling pointer)。悬空指针是指向已释放内存的指针,如果对其进行解引用操作,会导致未定义行为。
为了避免悬空指针问题,通常的做法是在调用 free() 之后,将指针设置为 NULL。这样可以确保程序不会误使用已释放的内存。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 动态分配内存
char *str = (char *)malloc(100 * sizeof(char));
if (str == NULL) {
printf("内存分配失败\n");
return 1;
}
// 使用分配的内存
snprintf(str, 100, "Hello, World!");
printf("%s\n", str);
// 释放动态分配的内存
free(str);
// 将指针设置为 NULL,避免悬空指针
str = NULL;
// 现在 str 已经是 NULL,安全地避免了悬空指针问题
if (str == NULL) {
printf("str is NULL\n");
}
return 0;
}
在这个示例中,str 指针在 free() 调用之后被设置为 NULL,这样就避免了悬空指针问题。如果尝试再次使用这个指针,由于它是 NULL,可以通过条件检查避免未定义行为。
atoi
int atoi(const char *str)
参数
- str -- 要转换为整数的字符串。
返回值
该函数返回转换后的长整数,如果没有执行有效的转换,则返回零。
实例
下面的实例演示了 atoi() 函数的用法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int val;
char str[20];
strcpy(str, "98993489");
val = atoi(str);
printf("字符串值 = %s, 整型值 = %d\n", str, val);
strcpy(str, "runoob.com");
val = atoi(str);
printf("字符串值 = %s, 整型值 = %d\n", str, val);
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
字符串值 = 98993489, 整型值 = 98993489
字符串值 = runoob.com, 整型值 = 0