把计算机的内存想象成一家酒店,每个房间就是一个内存单元,每个房间都有一个唯一的房间号,这个房间号就相当于内存地址。房间里可以存放客人的行李等物品,这些物品就好比存储在内存中的数据。
#include
int main()
{
int num = 10;
int *ptr = #
printf("变量 num 存储的内容(相当于房间里的物品): %d\n", num);
printf("变量 num 的地址(相当于房间号): %p\n", (void *)&num);
printf("指针 ptr 存储的地址(相当于记录了房间号的纸条): %p\n", (void *)ptr);
printf("通过指针 ptr 访问存储的内容(相当于根据纸条找到房间并查看里面的物品): %d\n", *ptr);
return 0;
}
num
的值。&num
就是获取 num
变量所在的内存地址,就如同获取酒店房间的房间号(例如 621)。ptr
存储了 num
的地址,类似于一张记录了房间号的纸条。我们可以通过这张纸条(指针)找到对应的房间(内存单元)并查看或修改里面的物品(数据)。计算机中的单位:
bit - 比特位
Byte -字节 1 Byte = 8 bit
KB -千字节 1 KB = 1024 Byte
MB - 兆字节 1 MB = 1024 KB
GB - 吉字节 1 GB = 1024 MB
TB - 太字节 1 TB = 1024 GB
.... ..............
int main()
{
int a = 4;
&a;// & -- 取地址操作符,拿到变量a的地址
printf("%p\n", &a);//%p是专门打印地址
int* pa = &a;//pa是一个变量,这个变量pa是用来存放地址(指针)的,pa叫指针变量
*pa = 200; * -- 解引用操作符(间接访问操作符)
// int* 是pa的类型 ,pa是指针变量的名字
// *表示pa是指针变量
//int表示pa指向的变量a的类型是int
return 0;
}
// 32位平台下地址是32个bit位(即4个字节)
// 64位平台下地址是64个bit位(即8个字节)
// 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
int main()
{ //在64位平台下地址是64个bit位(即8个字节)
printf("%zd\n", sizeof(char*)); //8
printf("%zd\n", sizeof(int*)); //8
printf("%zd\n", sizeof(short*)); //8
printf("%zd\n", sizeof(double*));//8
return 0;
}
/* 调试我们可以看到,代码1 会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。*/
//代码1:
int main()
{
int a = 0x12435644;//十六机进制每二位为一个字节12 43 56 44
int* pc = &a;
*pc = 0;//每次访问四个字节,所以一次性改了四个字节
return 0;
}
//代码2:
int main()
{
int a = 0x12435644;
char* pc = &a;
*pc = 0;//每次访问一个字节,所以一次性改了一个字节
return 0;
}
int main()
{
int a = 20;
int* pa = &a;
char* pc = &a;
printf(" &a = %p\n", &a); //&a = 00BAFC28
printf(" pa = %p\n", pa); //pa = 00BAFC28
printf(" pc = %p\n", pc); //pc = 00BAFC28
printf("&a+1 = %p\n", &a + 1); //&a+1 = 00BAFC2C 跟&a相差4个字节(多)
printf("pa+1 = %p\n", pa + 1); //pa+1 = 00BAFC2C 跟pa相差4个字节(多)
printf("pc+1 = %p\n", pc + 1); //pc+1 = 00BAFC29 跟pc相差1个字节(多)
printf("&a-1 = %p\n", &a-1); //&a-1 = 00BAFC24 跟&a相差4个字节(少)
printf("pa-1 = %p\n", pa-1); //pa-1 = 00BAFC24 跟&a相差4个字节(少)
printf("pc-1 = %p\n", pc-1); //pc-1 = 00BAFC27 跟&a相差1个字节(少)
return 0;
}
int* pa; pa+1 --> +1*sizeof(int)
pa+n --> +n*sizeof(int)
char* pa; pa+1 --> +1*sizeof(char)
pa+n --> +n*sizeof(char)
看了这么多了是不是还是有一点不懂?那我在这里跟你们举个例子:
int a =20;
int* pa = &a;
pa ,*pa ,&pa这三个有什么区别?
pa
pa
是一个指针变量,它存储的是另一个变量的内存地址。在上述代码中,pa
存储的是变量 a
的内存地址,即 pa
指向变量 a
。
pa
作为参数传递给函数,让函数能够通过这个地址访问或修改所指向的变量。例如: #include
void modifyValue(int *ptr) //传递过来的是地址所以必须得要指针来接
{
*ptr = 100;
}
int main() {
int a = 20;
int b = 100
int *pa = &a;
pa = &b;//pa换了一个地址,这时pa存储的就是b的地址
modifyValue(pa);//pa本身就是地址所以不用’&‘
printf("a 的值: %d\n", a); // 输出 100
return 0;
}
*pa
*
是解引用运算符,*pa
表示访问 pa
所指向的内存地址中存储的值。在上述代码中,*pa
访问的就是变量 a
的值。
*pa
获取 pa
所指向变量的值。例如: #include
int main()
{
int a = 20;
int *pa = &a;
printf("*pa 的值: %d\n", *pa); // 输出 20
return 0;
}
*pa
进行赋值操作,从而修改 pa
所指向变量的值。例如: #include
int main() {
int a = 20;
int *pa = &a;
*pa = 50;
printf("a 的值: %d\n", a); // 输出 50
return 0;
}
&pa
&
是取地址运算符,&pa
表示获取指针变量 pa
本身自己的内存地址。也就是说,&pa
是指向指针 pa
的指针。
&pa
赋值给一个二级指针(指向指针的指针)。例如: #include
int main()
{
int a = 20;
int *pa = &a;
int **ppa = &pa;
printf("通过二级指针访问 a 的值: %d\n", **ppa); // 输出 20
return 0;
}
在上述代码中,ppa
是一个二级指针,它存储的是指针 pa
的地址,通过两次解引用 **ppa
可以访问到变量 a
的值。
综上所述,pa
是指针,存储的是地址;*pa
是解引用操作,用于访问指针所指向的值;&pa
是取指针本身的地址,可用于创建二级指针等操作。
void*
指针是一种可以指向任何类型数据的指针。它不指定所指向数据的具体类型,只是表示一个内存地址。例如,可以定义一个 void*
指针如下:
void* ptr;
int
、float
等)和自定义数据类型(如结构体、联合体等)。这使得 void*
指针在一些通用的数据处理函数或数据结构中非常有用,能够实现对不同类型数据的统一操作。void*
指针不指定具体的数据类型,所以在使用它进行指针运算或访问所指向的数据时,需要进行显式的类型转换。因为编译器无法根据 void*
指针确定所指向数据的大小和类型,所以不能直接通过 void*
指针进行解引用操作来访问数据。void*
指针,可以使函数能够接受任意类型的数据指针作为参数,从而实现函数的通用性。例如,C 标准库中的 memcpy
函数就是使用 void*
指针来实现对任意类型数据的内存复制操作,其函数原型为 void* memcpy(void* dest, const void* src, size_t n);
,可以将 src
指向的内存区域中的 n
个字节复制到 dest
指向的内存区域中。void*
指针可以用来存储不同类型的数据。例如,链表节点可以使用 void*
指针来存储节点的数据,这样链表就可以存储任意类型的数据。void*
指针访问数据或进行指针运算时,必须进行显式的类型转换,将 void*
指针转换为正确的数据类型指针,否则可能会导致错误的访问或运算结果。void*
指针可以指向任意类型的数据,在进行内存分配和释放时要特别小心,确保正确地分配和释放内存,避免内存泄漏或悬空指针等问题。下面是一个简单的示例代码,演示了 void*
指针的基本用法:
#include
#include
// 函数接受void*指针作为参数,并根据传入的类型进行相应的操作
void printData(void* data, char type)
{
if (type == 'i')
{
// 将void*指针转换为int*指针,然后解引用输出
int* intPtr = (int*)data;
printf("Integer value: %d\n", *intPtr);
}
else if (type == 'f')
{
// 将void*指针转换为float*指针,然后解引用输出
float* floatPtr = (float*)data;
printf("Float value: %f\n", *floatPtr);
}
}
int main()
{
int intValue = 10;
float floatValue = 3.14f;
// 使用void*指针指向int类型数据
void* voidPtr1 = &intValue;
printData(voidPtr1, 'i');//i:代表 int
// 使用void*指针指向float类型数据
void* voidPtr2 = &floatValue;
printData(voidPtr2, 'f');//f:代表 float
return 0;
}
在上述代码中,printData
函数接受一个 void*
指针和一个字符类型的参数 type
,根据 type
的值将 void*
指针转换为相应类型的指针,并输出数据。在 main
函数中,分别使用 void*
指针指向一个 int
类型数据和一个 float
类型数据,并调用 printData
函数进行输出。
const type*
或 type const*
)const int* p;
int const* p; // 与上面的定义等价
#include
int main() {
int num = 10;
const int* p = #//因为*p被修饰限制了,但p并没有
// *p = 20; // 错误,不能通过指向常量的指针修改数据
int anotherNum = 30;
p = &anotherNum; // 正确,指针本身可以指向其他地址
return 0;
}
type* const
)int* const p;
这里定义了一个 int
类型的常量指针 p
,在初始化后,p
就只能指向它初始化时所指向的那个地址,不能再指向其他地址,但可以通过 p
来修改其所指向的 int
类型数据的值。
#include
int main()
{
int num1 = 10;
int num2 = 20;
int* const p = &num1;
*p = 30; // 正确,可以通过常量指针修改所指向的数据
// p = &num2; // 错误,常量指针不能改变指向
return 0;
}
const type* const
)const int* const ptr;
#include
int main()
{
int num = 10;
const int* const ptr = #//这里p和p*都被限制
// *p = 20; // 错误,不能通过指针修改数据
// p = &anotherNum; // 错误,指针不能改变指向
return 0;
}
运算规则:当一个指针加上或减去一个整数时,实际上是在内存中向前或向后移动了若干个所指向数据类型大小的位置。移动的字节数等于整数乘以指针所指向数据类型的字节数。
示例代码
#include
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 指针ptr指向数组arr的首元素
// 指针加整数
int *ptr1 = ptr + 2; // 指针向后移动2个int类型的位置
printf("ptr1指向的值: %d\n", *ptr1); // 输出3
// 指针减整数
int *ptr2 = ptr1 - 1; // 指针向前移动1个int类型的位置
printf("ptr2指向的值: %d\n", *ptr2); // 输出2
return 0;
}
代码解释:在上述代码中,int
类型通常占 4 个字节。ptr + 2
表示指针向后移动 2 * sizeof(int)
个字节,即指向数组中的第 3 个元素;ptr1 - 1
表示指针向前移动 1 * sizeof(int)
个字节,即指向数组中的第 2 个元素。
运算规则:只有当两个指针指向同一个数组中的元素时,才能进行指针相减运算。指针相减的结果是两个指针之间相隔的元素个数,其类型为 ptrdiff_t
(在
头文件中定义)。 示例代码
#include
#include
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
ptrdiff_t diff = ptr2 - ptr1; // 计算两个指针之间相隔的元素个数
printf("ptr2和ptr1之间相隔的元素个数: %td\n", diff); // 输出3
return 0;
}
代码解释:ptr2
指向数组的第 4 个元素,ptr1
指向数组的第 1 个元素,它们之间相隔 3 个元素,所以 ptr2 - ptr1
的结果为 3。
运算规则:指针的关系运算包括 <
、>
、<=
、>=
、==
和 !=
等。这些运算通常用于比较两个指针是否指向同一个位置,或者判断一个指针是否在另一个指针之前或之后。同样,只有当两个指针指向同一个数组中的元素时,这些关系运算才有意义。
示例代码
#include
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
if (ptr1 < ptr2)
{
printf("ptr1在ptr2之前\n");
}
else
{
printf("ptr1不在ptr2之前\n");
}
if (ptr1 != ptr2)
{
printf("ptr1和ptr2不指向同一个位置\n");
}
else
{
printf("ptr1和ptr2指向同一个位置\n");
}
return 0;
}
代码解释:在上述代码中,由于 ptr1
指向数组的第 1 个元素,ptr2
指向数组的第 4 个元素,所以 ptr1 < ptr2
条件成立,ptr1 != ptr2
条件也成立。
定义
野指针是指指向一个无效的、未分配或者已经释放的内存地址的指针。这种指针没有明确指向一个合法的内存区域,对其进行解引用操作(即访问其所指向的内存内容)会产生不可预期的结果。
当定义一个指针变量但没有对其进行初始化时,该指针会包含一个随机的内存地址,这个地址可能并不指向任何有效的内存区域,从而成为野指针。
#include
int main()
{
int *ptr; // 定义指针但未初始化
// *ptr = 10; // 错误:对野指针进行解引用操作
return 0;
}
NULL
当使用 malloc()
、calloc()
或 realloc()
等函数动态分配内存,并使用指针指向这块内存后,如果后续使用 free()
函数释放了该内存,但没有将指针置为 NULL
,那么这个指针就会变成悬空指针也被称为野指针。
#include
#include
int main()
{
int *ptr = (int *)malloc(sizeof(int));//申请动态内存
if (ptr == NULL)
{
return 1;
}
*ptr = 10;
free(ptr); // 释放内存
// ptr 现在是野指针
// *ptr = 20; // 错误:对野指针进行解引用操作
return 0;
}
当指针指向数组元素时,如果对指针进行了超出数组范围的操作,使得指针指向了数组之外的内存区域,那么该指针就可能成为野指针。
#include
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
ptr = ptr + 10; // 指针越界,指向了数组之外的内存区域
// *ptr = 10; // 错误:对野指针进行解引用操作
return 0;
}
当一个指针指向一个局部变量时,如果这个局部变量所在的函数执行完毕,局部变量的生命周期结束,其占用的内存会被释放。但此时指针仍然保留着该局部变量的地址,就会成为悬空指针。
#include
int* add(int x,int y)
{
int z = x + y;
return &z; // 返回局部变量的地址
}
int main()
{
int a = 3;
int b = 5;
int *ptr = add(a,b);
// 此时ptr成为悬空指针,因为z已经超出作用域
// *ptr = 20; // 错误:对悬空指针进行解引用操作
return 0;
}
程序崩溃:对野指针进行解引用操作时,可能会访问到操作系统保护的内存区域,从而导致程序崩溃。
数据损坏:野指针可能指向了其他程序或数据结构正在使用的内存区域,对其进行写操作会破坏这些数据,导致程序出现不可预期的错误。
在定义指针变量时,尽量对其进行初始化。如果暂时不知道要指向哪里,可以将其初始化为 NULL
。
#include
int main()
{
int *ptr = NULL; // 初始化指针为NULL
int num = 10;
ptr = # // 让指针指向有效的内存地址
*ptr = 20;
printf("%d\n", *ptr);
return 0;
}
NULL
在使用 free()
函数释放动态分配的内存后,及时将指针置为 NULL
,避免再次使用该指针时产生错误。
#include
#include
int main()
{
int *ptr = (int *)malloc(sizeof(int));//申请动态空间
if (ptr == NULL)
{
return 1;
}
*ptr = 10;
free(ptr);
ptr = NULL; // 将指针置为NULL
// 此时再使用ptr就可以先判断是否为NULL
if (ptr != NULL)
{
*ptr = 20;
}
return 0;
}
在使用指针访问数组元素时,要确保指针不会超出数组的有效范围。可以通过合理的边界检查来避免指针越界。
#include
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++)
{
printf("%d ", *(ptr + i)); // 确保指针在数组有效范围内
}
return 0;
}
assert
是一个宏,定义在
头文件中。其原型如下:
void assert(int expression);
这里的 expression
是一个需要检查的条件表达式,如果该表达式的值为 0
(即条件为假),则 assert
会触发断言失败,程序会终止运行;如果表达式的值为非 0
(即条件为真),则程序会继续正常执行。
以下是一个简单的使用 assert
的示例代码:
#include
#include
// 一个简单的除法函数
int divide(int a, int b)
{
// 使用assert检查除数是否为0
assert(b != 0);
return a / b;
}
int main()
{
int result1 = divide(10, 2);
printf("10 / 2 = %d\n", result1);
// 这会触发断言失败
int result2 = divide(10, 0);
printf("10 / 0 = %d\n", result2);
return 0;
}
在上述代码中,divide
函数使用 assert
检查除数 b
是否为 0
。当调用 divide(10, 2)
时,条件 b != 0
为真,程序会正常计算并输出结果;而当调用 divide(10, 0)
时,条件 b != 0
为假,assert
会触发断言失败,程序会终止运行,并输出类似下面的错误信息:
Assertion failed: b != 0, file test.c, line 6
该信息会指出断言失败的条件、所在的文件名和行号,方便开发者定位问题。
在程序编译时,assert
宏会被展开为一段代码。当程序运行到 assert
语句时,会先计算传入的条件表达式的值。如果值为 0
,则会调用 abort()
函数终止程序,并输出包含断言条件、文件名和行号的错误信息;如果值为非 0
,则程序会继续正常执行。
assert
在发布版本的程序中,为了避免 assert
带来的性能开销,可以通过定义 NDEBUG
宏来禁用 assert
。例如:
#define NDEBUG
#include
#include
int main() {
int num = 10;
assert(num == 20); // 由于NDEBUG被定义,此断言不会生效
printf("Program continues...\n");
return 0;
}
在上述代码中,由于在包含
头文件之前定义了 NDEBUG
宏,assert
宏会被展开为空代码,不会进行条件检查,从而提高程序的运行效率。
assert
可以在程序运行时自动检查条件,当条件不满足时会立即终止程序并给出详细的错误信息,帮助开发者快速定位和解决问题。assert
可以在代码中简洁地表达对程序状态的预期,使代码更易读和维护。assert
语句处都需要计算条件表达式的值,这会带来一定的性能开销,尤其是在条件检查比较复杂或者程序中有大量 assert
语句时。assert
可以通过定义 NDEBUG
宏来禁用,因此它只能用于调试阶段,不能用于处理程序运行时可能出现的错误(如用户输入错误、文件打开失败等),这些错误需要使用其他机制(如错误码、异常处理等)来处理。size_t strlen ( const char * str );
size_t
是无符号整数类型,通常用于表示对象的大小或长度。const char *str
表示传入的是一个指向常量字符的指针,这意味着函数内部不会修改字#include
// 模拟实现strlen函数
size_t my_strlen(const char *str)
{
const char *ptr = str; // 定义一个指针ptr指向字符串的首地址
while (*ptr != '\0') // 当指针指向的字符不是字符串结束符时
{
ptr++; // 指针向后移动一位
}
return ptr - str; // 计算指针ptr和str之间的差值,即为字符串的长度
}
int main()
{
char str[] = "Hello, World!";
size_t length = my_strlen(str); //调用自定义的my_strlen函数计算字符串的长度,数组名就是地址
printf("字符串 \"%s\" 的长度是: %zu\n", str, length);
return 0;
}
函数实现:
const char *ptr = str;
while ( *ptr != '\0' ) {
ptr++;
}
return ptr - str;
ptr
并将其初始化为 str
,即指向字符串的首地址。while
循环遍历字符串,当 ptr
指向的字符不是字符串结束符 '\0'
时,将 ptr
向后移动一位。ptr
和 str
之间的差值,即为字符串的长度。传址调用:
size_t length = my_strlen(str);
main
函数中,将字符串 str
的首地址传递给 my_strlen
函数,这就是传址调用。通过传址调用,函数可以直接访问和操作原始字符串,而不是创建字符串的副本。