知识点总览:
目录
一、内存和地址
1.1内存:
定义:
内存(也称作主存或内存储器,英文为Memory)是计算机系统中的一种重要硬件设备,用于临时存储正在处理的数据和程序代码。它是中央处理器(CPU)可以直接访问的存储空间,与外部存储设备(如硬盘、固态硬盘)相比,内存的读取和写入速度更快,但其存储容量相对较小,且断电后会丢失数据。
注:在电脑中,我们所熟知的C盘D盘都不是内存,它们是外部存储设备,不要混淆。
现在我们将内存形象化:
公寓楼和单元
- 公寓楼(内存):想象内存是一栋大型公寓楼,每个房间代表内存中的一个存储位置。公寓楼的总面积就像内存的总容量。
- 房间(内存单元):每个房间可以住一定的人数,就像内存中的每个单元存储一个字节。这些单元是存放数据和指令的地方。为了方便管理内存,我们将内存划分成一个个单元。每个内存单元的大小取1字节。
地址:
- 房间号(内存地址):每个房间都有一个唯一的门牌号,用来区分不同的单元。内存地址(Memory Address)也是如此,每个内存单元都有一个唯一的地址,用于标识和访问该位置的数据
图像化:
在计算机中我们将内存单元编号也成为地址,在C语言中给地址起名为指针
则内存单元的编号==地址==指针
内存的大小:
最小的单位:比特(bit) 只能存储0或1
字节(Byte,B)
1B =8Bit
千字节(KB)
1 KB =1024 B
兆字节(MB)
1 MB = 1024 KB = 1,048,576 字节 (B)
吉字节(GB)
1 GB = 1024 MB = 1,073,741,824 字节 (B)
太字节(TB)
1 TB = 1024 GB = 1,099,511,627,776 字节 (B)
1.2理解编址:
首先,我们需要知道为什么要编址。因为CPU访问内存中的数据,需要通过地址找到相应的内存空间,CPU和内存通过地址总线,来进行相互的操作。
一根线能表示两种含义,0,1(电脉冲有无)。在32位机器下有32跟地址总线,那么通过32跟地址总线就能表达出2^32中含义。每一种含义都代表一个地址。
二、指针变量和地址
2.1取地址操作符(&)
每一个变量定义时,都想内存申请了空间。那么每个变量对应的内存都会一个地址。我们通过&操作符,就可以得到该变量内存所对应地址。
基本用法
-
获取变量的地址:
int a = 10;
int *p = &a; // p是一个指针,存储变量a的地址
2.传递指针给函数:
void updateValue(int *p) {
*p = 20; // 改变指针指向的变量的值
}
int main() {
int a = 10;
updateValue(&a); // 传递变量a的地址
printf("%d\n", a); // 输出20
return 0;
}
示例代码
以下是一个完整的示例,演示如何使用取地址操作符和指针:
#include <stdio.h>
// 函数声明:通过指针修改变量的值
void changeValue(int *p);
int main() {
int num = 5;
// 打印变量的地址和值
printf("Address of num: %p\n", &num);
printf("Value of num: %d\n", num);
// 调用函数并传递变量的地址
changeValue(&num);
// 打印修改后的值
printf("New value of num: %d\n", num);
return 0;
}
// 函数定义:通过指针修改变量的值
void changeValue(int *p) {
*p = 10; // 通过指针修改变量的值
}
输出结果
运行上述代码,输出如下:
Address of num: 0x7ffeedc2c9a8
Value of num: 5
New value of num: 10
说明
&num
用于获取变量num
的地址。int *p = #
创建一个指针p
,它保存了num
的地址。- 在
changeValue
函数中,*p = 10;
修改了指针p
指向的变量的值。
取地址操作符 &
在指针操作和函数参数传递中非常重要,特别是在需要通过指针修改变量值或需要访问数组元素时。
指针类型
指针类型决定了指针变量所指向的数据类型。它由基础类型和一个或多个星号(*
)组成。例如:
int *p;
表示p
是一个指向整数类型的指针。char *p;
表示p
是一个指向字符类型的指针。double *p;
表示p
是一个指向双精度浮点类型的指针。
指针类型不仅决定了指针变量所指向的值的类型,还影响了指针的算术操作。例如,对于一个 int
指针,加1将增加该指针的地址,增加的量为一个 int
类型所占用的字节数。
解引用操作符 (*
)
解引用操作符 *
用于访问指针指向的内存地址中的值。它可以用于读取和修改该内存位置的值。例如:
int a = 5;
int *p = &a;
printf("Value of a: %d\n", *p); // 输出 5
*p = 10; // 通过指针修改变量a的值
printf("New value of a: %d\n", a); // 输出 10
指针变量的大小:
在C语言中,指针变量的大小通常与指针所指向的数据类型无关,而与编译器和操作系统的体系结构相关。指针变量的大小实际上是由系统的内存地址长度决定的。例如,在32位系统中,指针变量的大小通常是4字节,而在64位系统中,指针变量的大小通常是8字节。
示例代码
以下是一个示例代码,展示如何确定不同类型指针的大小:
#include <stdio.h>
int main() {
int *int_ptr;
char *char_ptr;
double *double_ptr;
void *void_ptr;
printf("Size of int pointer: %zu bytes\n", sizeof(int_ptr));
printf("Size of char pointer: %zu bytes\n", sizeof(char_ptr));
printf("Size of double pointer: %zu bytes\n", sizeof(double_ptr));
printf("Size of void pointer: %zu bytes\n", sizeof(void_ptr));
return 0;
}
输出结果
在64位系统上编译和运行上述代码,输出可能如下:
Size of int pointer: 8 bytes
Size of char pointer: 8 bytes
Size of double pointer: 8 bytes
Size of void pointer: 8 bytes
在32位系统上,输出可能如下:
Size of int pointer: 4 bytes
Size of char pointer: 4 bytes
Size of double pointer: 4 bytes
Size of void pointer: 4 bytes
说明
sizeof(int_ptr)
返回指向int
类型的指针的大小。sizeof(char_ptr)
返回指向char
类型的指针的大小。sizeof(double_ptr)
返回指向double
类型的指针的大小。sizeof(void_ptr)
返回指向void
类型的指针的大小。
尽管这些指针指向不同的数据类型,但它们的大小在同一平台上是相同的。这是因为指针的大小由地址空间决定,而不是由所指向的数据类型决定。
体系结构对指针大小的影响
- 32位系统:使用32位地址空间,因此指针的大小通常是4字节。
- 64位系统:使用64位地址空间,因此指针的大小通常是8字节。
三、指针变量类型的意义
指针与整数的加减法
1. 指针加整数
当指针 p
加上一个整数 n
时,结果是指针 p
指向的内存地址向后移动 n
个元素的位置。具体的移动量是 n
乘以指针类型的大小(以字节为单位)。
int *p;
p + n; // 等效于 p + n * sizeof(int)
2. 指针减整数
当指针 p
减去一个整数 n
时,结果是指针 p
指向的内存地址向前移动 n
个元素的位置。具体的移动量是 n
乘以指针类型的大小(以字节为单位)。
int *p;
p - n; // 等效于 p - n * sizeof(int)
示例代码
以下是一些示例代码,展示了指针与整数的加减法操作:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // 指向数组的第一个元素
// 指针加整数
printf("Address of p: %p, Value: %d\n", p, *p); // 输出数组第一个元素地址和值
p = p + 2;
printf("Address of p + 2: %p, Value: %d\n", p, *p); // 输出数组第三个元素地址和值
// 指针减整数
p = p - 1;
printf("Address of p - 1: %p, Value: %d\n", p, *p); // 输出数组第二个元素地址和值
return 0;
}
输出结果
运行上述代码,输出如下:
Address of p: 0x7ffeedc2c9a8, Value: 10
Address of p + 2: 0x7ffeedc2c9b0, Value: 30
Address of p - 1: 0x7ffeedc2c9a8, Value: 20
说明
-
初始化指针
p
指向数组的第一个元素:int *p = arr;
-
指针加整数:
p = p + 2;
- 指针
p
从arr
指向的第一个元素移动到第三个元素。 - 输出
p
的新地址和值(第三个元素的值)。
- 指针
-
指针减整数:
p = p - 1;
- 指针
p
从第三个元素移动到第二个元素。 - 输出
p
的新地址和值(第二个元素的值)。
- 指针
总结
- 指针加上一个整数
n
会移动指针n
个元素位置,具体移动量是n
乘以指针类型的大小。 - 指针减去一个整数
n
会移动指针n
个元素位置,具体移动量是n
乘以指针类型的大小。 - 指针的加减法主要用于数组操作和遍历内存块。
void*
指针的使用
1. 声明和赋值
可以将任何类型的指针赋值给 void*
指针,而无需进行类型转换:
int a = 10;
char c = 'A';
double d = 3.14;
void *ptr;
ptr = &a; // 指向int类型
ptr = &c; // 指向char类型
ptr = &d; // 指向double类型
. 解引用 void*
指针
因为 void*
指针不指定指向数据的类型,所以在解引用之前需要进行类型转换:
int a = 10;
void *ptr = &a;
// 解引用前需要进行类型转换
int *int_ptr = (int *)ptr;
printf("Value: %d\n", *int_ptr); // 输出10
3. 示例代码
以下是一个示例,展示如何使用 void*
指针来实现通用的打印函数:
#include <stdio.h>
void printValue(void *ptr, char type) {
switch(type) {
case 'i': // int
printf("Value: %d\n", *(int *)ptr);
break;
case 'c': // char
printf("Value: %c\n", *(char *)ptr);
break;
case 'd': // double
printf("Value: %lf\n", *(double *)ptr);
break;
default:
printf("Unsupported type\n");
break;
}
}
int main() {
int a = 10;
char c = 'A';
double d = 3.14;
printValue(&a, 'i'); // 输出 int 类型值
printValue(&c, 'c'); // 输出 char 类型值
printValue(&d, 'd'); // 输出 double 类型值
return 0;
}
输出结果
运行上述代码,输出如下:
Value: 10
Value: A
Value: 3.140000
四、const修饰指针
1. 修饰基本数据类型
当 const
修饰基本数据类型时,变量的值在初始化后不能被修改。
#include <stdio.h>
int main() {
const int a = 10;
// a = 20; // 错误:不能修改 const 变量的值
printf("Value of a: %d\n", a);
return 0;
}
2. 修饰指针
当 const
修饰指针时,可以有几种不同的用法,根据 const
位置的不同含义也不同:
2.1 指向常量的指针
指针指向的值不能被修改,但指针本身可以指向不同的地址。
int main() {
int a = 10;
int b = 20;
const int *p = &a; // p 指向一个常量整数
// *p = 20; // 错误:不能通过指针修改指向的值
p = &b; // 可以修改指针指向的地址
printf("Value of *p: %d\n", *p); // 输出 20
return 0;
}
2.2 常量指针
指针本身是常量,不能指向其他地址,但可以通过指针修改指向的值。
int main() {
int a = 10;
int b = 20;
int * const p = &a; // p 是一个常量指针
*p = 20; // 可以通过指针修改指向的值
// p = &b; // 错误:不能修改指针指向的地址
printf("Value of *p: %d\n", *p); // 输出 20
return 0;
}
2.3 指向常量的常量指针
指针本身和指向的值都不能被修改。
int main() {
int a = 10;
const int * const p = &a; // p 是一个指向常量的常量指针
// *p = 20; // 错误:不能通过指针修改指向的值
// p = &b; // 错误:不能修改指针指向的地址
printf("Value of *p: %d\n", *p); // 输出 10
return 0;
}
总结
const
修饰基本数据类型时,变量的值不能被修改。const
修饰指针时,可以有多种组合:const int *p
:指向常量的指针,指针指向的值不能被修改。int * const p
:常量指针,指针本身不能修改指向的地址。const int * const p
:指向常量的常量指针,指针本身和指向的值都不能被修改。
五、指针运算
指针减法运算
指针减法运算的基本形式是 p1 - p2
,其中 p1
和 p2
是指向同一数组的指针。减法运算的结果是这两个指针之间的元素差距,而不是字节差距。
示例代码
以下是一个示例,展示了指针减法的使用:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1]; // 指向数组第二个元素
int *p2 = &arr[4]; // 指向数组第五个元素
// 计算指针之间的差距
int diff = p2 - p1;
printf("Pointer p1 points to: %d\n", *p1);
printf("Pointer p2 points to: %d\n", *p2);
printf("Difference (p2 - p1): %d\n", diff);
return 0;
}
注:在后续模拟实现strlen中也将是非常有帮助的
指针关系运算的基本规则
- 指针必须指向同一块内存:进行关系运算的两个指针必须指向同一个数组或内存块的元素。否则,结果是未定义行为。
- 关系运算符:可以使用以下关系运算符对指针进行比较:
==
:判断两个指针是否相等。!=
:判断两个指针是否不相等。<
:判断一个指针是否小于另一个指针。>
:判断一个指针是否大于另一个指针。<=
:判断一个指针是否小于或等于另一个指针。>=
:判断一个指针是否大于或等于另一个指针。
示例代码
以下是一些示例代码,展示了指针关系运算的使用:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1]; // 指向数组第二个元素
int *p2 = &arr[3]; // 指向数组第四个元素
int *p3 = &arr[1]; // 也指向数组第二个元素
// 比较指针
if (p1 == p3) {
printf("p1 and p3 point to the same element.\n");
} else {
printf("p1 and p3 do not point to the same element.\n");
}
if (p1 != p2) {
printf("p1 and p2 point to different elements.\n");
}
if (p1 < p2) {
printf("p1 points to an element before the element pointed to by p2.\n");
}
if (p2 > p1) {
printf("p2 points to an element after the element pointed to by p1.\n");
}
return 0;
}
输出结果
运行上述代码,输出如下:
p1 and p3 point to the same element.
p1 and p2 point to different elements.
p1 points to an element before the element pointed to by p2.
p2 points to an element after the element pointed to by p1.
说明
-
指针相等比较 (
==
):if (p1 == p3) { printf("p1 and p3 point to the same element.\n"); }
p1
和p3
都指向arr[1]
,所以它们相等。
-
指针不等比较 (
!=
):if (p1 != p2) { printf("p1 and p2 point to different elements.\n"); }
p1
指向arr[1]
,p2
指向arr[3]
,所以它们不相等。
-
指针小于比较 (
<
):if (p1 < p2) { printf("p1 points to an element before the element pointed to by p2.\n"); }
p1
指向arr[1]
,p2
指向arr[3]
,所以p1
小于p2
。
-
指针大于比较 (
>
):if (p2 > p1) { printf("p2 points to an element after the element pointed to by p1.\n"); }
p2
指向arr[3]
,p1
指向arr[1]
,所以p2
大于p1
。
六、野指针
野指针是指指向无效内存地址的指针,这可能导致程序崩溃、内存泄漏或未定义的行为。野指针的成因主要有以下几种:
-
未初始化的指针:在声明指针变量但未初始化时,指针变量的值是不确定的,它可能指向任意地址,包括无效的地址。
int *ptr; // 未初始化的指针
2.释放后未置空的指针:释放内存后,未将指针置为空,导致指针仍然指向之前的内存位置,此时访问该指针会引发野指针问题。
int *ptr = malloc(sizeof(int));
free(ptr);
// 未置空的指针
3.指针操作错误:指针操作错误可能导致指针指向无效内存地址,例如超出数组边界、释放了栈上的变量等。
int arr[5];
int *ptr = &arr[10]; // 超出数组边界
避免野指针的出现是至关重要的,可以采取以下措施来规避野指针:
-
初始化指针:在声明指针变量时,及时初始化为
NULL
或有效地址。
int *ptr = NULL; // 初始化为 NULL
2.合理释放内存:在使用动态内存分配函数(如 malloc()
、calloc()
、realloc()
)分配内存后,确保在不再使用该内存时调用 free()
函数释放内存,并将指针置为 NULL
。
int *ptr = malloc(sizeof(int));
// 使用 ptr
free(ptr);
ptr = NULL; // 置空指针
3.谨慎进行指针操作:避免超出数组边界、释放栈上的变量等操作,确保指针指向的内存地址是有效的。
int arr[5];
int *ptr = &arr[0]; // 正确的指针操作
七、assert断言
在C语言中,assert
是一个宏,用于在程序中插入断言,即在程序运行时检查某个条件是否为真。如果条件为假(即“断言失败”),则程序会终止并打印出错信息。assert
宏定义在 <assert.h>
头文件中。
使用示例
以下是一个使用 assert
的简单示例:
#include <stdio.h>
#include <assert.h>
int main() {
int x = 10;
int y = 20;
// 断言 x 等于 y
assert(x == y);
printf("Program continues after assertion.\n");
return 0;
}
在上面的示例中,assert(x == y);
语句会检查变量 x
是否等于变量 y
。由于 x
和 y
不相等,该断言会失败,导致程序终止执行,并输出错误信息:
Assertion failed: (x == y), file example.c, line 9.
注意事项
assert
宏通常用于在开发和调试阶段对程序的内部逻辑进行检查,一般不在生产环境中使用。因为在生产环境中,断言失败会导致程序异常终止,可能会影响程序的稳定性和可用性。- 断言失败时,
assert
宏会打印出错信息,并输出失败的条件表达式、文件名和行号,帮助定位问题。 - 在编译程序时,可以通过定义
NDEBUG
宏来禁用assert
宏,这样所有的assert
断言都会被移除,不会被包含在编译后的代码中。
#define NDEBUG
八、指针的使用和传址调用
strlen
函数用于计算字符串的长度,即字符串中字符的个数,不包括结尾的空字符 '\0'
。以下是 strlen
函数的简单模拟实现:
传址调用 vs 传值调用
传址调用允许函数修改原始变量的值,而传值调用仅仅是传递了变量的副本给函数,函数对副本的修改不会影响到原始变量。
#include <stdio.h>
size_t strlen_simulate(const char *str) {
size_t length = 0; // 初始化字符串长度为 0
// 遍历字符串,直到遇到结尾的空字符 '\0'
while (*str != '\0') {
length++; // 每遇到一个字符,长度加一
str++; // 指针指向下一个字符
}
return length; // 返回字符串长度
}
int main() {
const char *str = "Hello, World!";
size_t length = strlen_simulate(str);
printf("Length of the string: %zu\n", length);
return 0;
}
也可以使用指针减去指针的方式进行计算:
#include <stdio.h>
size_t strlen_simulate(const char *str) {
char* end=str;//找到
// 遍历字符串,直到遇到结尾的空字符 '\0'
while (*end != '\0') {
end++;
}
return end-str; // 返回字符串长度
}
int main() {
const char *str = "Hello, World!";
size_t length = strlen_simulate(str);
printf("Length of the string: %zu\n", length);
return 0;
}
传值调用示例
#include <stdio.h>
void modifyValue(int x) {
x = 20; // 修改参数 x 的值
}
int main() {
int x = 10;
printf("Before modification: %d\n", x); // 输出原始值
modifyValue(x); // 传递变量 x 的值
printf("After modification: %d\n", x); // 输出原始值,不会受到函数修改的影响
return 0;
}
传址调用
在C语言中,函数参数默认是按值传递的,即函数接收的是实参的一个副本。但是通过指针传递参数,可以实现传址调用,即将变量的地址传递给函数,使得函数可以直接修改原始变量的值。
传址调用示例
#include <stdio.h>
void modifyValue(int *ptr) {
*ptr = 20; // 修改指针所指向的值
}
int main() {
int x = 10;
printf("Before modification: %d\n", x); // 输出原始值
modifyValue(&x); // 传递变量 x 的地址
printf("After modification: %d\n", x); // 输出修改后的值
return 0;
}