一.内存和地址
说到内存,内存就像一栋宿舍楼,而每一楼都有这十几二十个房间,每个房间都能住好几个人。那如果我们需要寻找某一个房间的时候,我们需要怎么找呢?答案自然是通过房间号来找了。房间号我们也称之为地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字:指针。
相对的:
内存相当于一栋宿舍楼
内存单元相当于一个房间(每个内存单元取1个字节)
比特位相当于一个学生(1个字节能放8个比特位)
就像这样:
所以内存单元编号=地址=指针
二.指针变量和地址
1.取地址操作符(&)
在C语⾔中创建变量其实就是向内存申请空间
#include<stdio.h>
int main() {
int a = 10;
printf("%p", &a);
return 0;
}
我们打印出a的地址,如图:
再看一下他的内存所在,可以看到,其一共占用4个字节,因为a是int型的,而且值也恰好是10(0a十六进制转换为十进制为10)。
2.指针变量和解引⽤操作符(*)
2.1指针变量
我们在上面介绍了&操作符,那我们拿到了地址要怎么存放呢?
答案是用指针变量。
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
#include<stdio.h>
int main() {
int a = 10;
int* pa = &a;//指针变量存放地址
printf("%p\n", &a);
printf("%p", pa);
return 0;
}
可以看到两地址都相等:
2.2如何理解指针类型
int a = 10;
int* pa = &a;
‘ *** '代表着pa是一个指针变量**
int则说明pa是整型类型的
**(注意:*可以写在左边一点,也可以右边一点,都是正确的),**如:
int *pa=&a;
int* pa=&a;
指针类型不仅仅只有int型的还有:
字符型指针charchar* p1短整型指针shortshort* p1整型指针intint* p1长整型指针longlong* p1单精度浮点型指针floatfloat* p1双精度浮点型指针doubledouble* p1
(注意:指针变量的类型要与变量的基本类型相同)
对于这么多的类型,我们来查看一下他们的内存大小:
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(float*));
printf("%d\n",sizeof(double*));
return 0;
}
不同平台的运行结果:
总结:
** 无论是哪一种平台下计算的结果,每种指针类型的内存大小都一样,都是4或8个字节。**
2.3解引用操作符
解引用操作符用于获取指针所指向的对象或变量的值。
解引⽤操作符(*****)。
两个例子:
int x = 10;
int* ptr = &x; // ptr 是指向 x 的指针
int y = *ptr; // 解引用 ptr,获取 x 的值,y 现在是 10
int a = 100;
int* pa = &a;//pa指向a的地址
*pa = 0;//解引用pa,通过pa中存放的地址,找到指向的空间,
//*pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0
至于为什么弄这么复杂,直接一点定义一个变量直接赋值,或者直接让它等于零就行了,为什么还有多此一举绕来绕去呢?
当然我们可以这么做,但是的话我们多一种方法多一种途径来给他赋值或者干嘛的,何乐而不为呢,学会之后届时我们写代码的时候就可以更加灵活了。
2.4 指针的解引用
对比下面两个代码:
(1)
#include <stdio.h>
int main()
{
int n = 0x11223344;十六进制转换为十进制结果为287454020
int* pi = &n;
printf("%p\n", pi);
*pi = 0;
printf("%d", n);//0
return 0;
}
结果为:
pi在内存中的地址和字节:
(2)
#include <stdio.h>
int main()
{
int n = 0x11223344;//十六进制转换为十进制结果为287454020
char* pc = (char*)&n;//强制转换为char*型
printf("%p\n", pc);
*pc = 0;
printf("%d", n);//287453952
return 0;
}
结果为:
'
pc在内存中的地址和字节:
通过调试我们可以看到,代码(1)会将n的4个字节全部改为0,但是代码(2)只是将n的第⼀个字节改为0。
**总结: 指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。 ⽐如: char 的指针解引⽤就只能访问⼀个字节,⽽ int 的指针的解引⽤就能访问四个字节。
2.5.不同指针类型的运加减性质
在许多编程语言中(例如C和C++),指针与整数相加或相减是一个常见的操作。这个操作可以用来在内存中遍历数组或数据结构。
2.5.1指针与整数相加:
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr 指向数组的第一个元素,即 arr[0].注意单个数组名一般指向数组的第一个元素
ptr = ptr + 2; // 现在 ptr 指向 arr[2],即 30
解释:
- 当你将一个指针与一个整数相加时,结果是一个新的指针,它指向原始指针指向的内存地址之后的某个位置。
- 如果指针指向的是一个数组的元素,那么指针加上整数
n
将指向数组中从当前元素开始的第n
个元素。
2.5.2指针与整数相减:
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = &arr[3]; // ptr 指向数组的第四个元素,即 arr[3]
ptr = ptr - 2; // 现在 ptr 指向 arr[1],即 20
解释:
- 当你将一个指针与一个整数相减时,结果是一个新的指针,它指向原始指针指向的内存地址之前的某个位置。
- 如果指针指向的是一个数组的元素,那么指针减去整数
n
将指向数组中从当前元素开始的第n
个之前的元素。
2.5.3指针运算的实际地址:
解释:
- 由于指针运算考虑了指针所指向数据类型的大小(
sizeof(类型)
),这意味着ptr + 1
实际上是增加了sizeof(类型)
个字节,而不是简单的增加 1。 - 例如,如果
ptr
是一个int*
类型的指针,假设int
类型占用 4 个字节,那么ptr + 1
实际上是将ptr
的地址增加了 4 个字节。
代码示例:
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
结果如图:
2.5.4指针运算的用法:
遍历数组:指针运算可以用于遍历数组中的元素。
#include <stdio.h>
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
int* ptr = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i)); // 输出数组中的每一个元素
}
return 0;
结果如图:
指向结构体成员:指针运算还可以用于遍历结构体数组中的元素。
三.void*指针
void*
指针在C和C++中是一种通用指针类型,表示它可以指向任意类型的数据。
void*
指针本身不包含类型信息,只是一个内存地址,因此不能直接解引用或进行指针运算。
在将
void*
指针传递给其他函数时,通常需要将其转换为具体类型的指针。类型转换使用类型转换运算符
(type*)
。
void* ptr;
int x = 10;
ptr = &x; // void* 指向 int 类型变量
int* intPtr = (int*)ptr; // 将 void* 转换为 int* 类型
printf("%d\n", *intPtr); // 解引用 int* 类型指针,输出 10
void*
指针常用于需要接受不同类型数据的函数参数。- 例如,一个通用的比较函数可以使用
void*
指针来比较不同类型的值:
int compare(const void* a, const void* b) {
return (*(int*)a - *(int*)b);
}
qsort(arr, 5, sizeof(int), compare); // 使用 qsort 排序 int 类型数组
注意事项:
** 不能直接解引用**:
- 由于
void*
不包含类型信息,不能直接对其进行解引用操作。必须先将其转换为具体类型的指针,然后才能解引用。 - 错误示例:
void* ptr;int x = 10;ptr = &x;// printf("%d\n", *ptr); // 错误:void* 不能直接解引用
不能进行指针运算:- 由于
void*
指针没有确定的类型大小,不能进行指针算术运算(如ptr + 1
)。必须将其转换为具体类型指针后再进行运算。 - 错误示例:
void* ptr;int arr[5] = {1, 2, 3, 4, 5};ptr = arr;// ptr++; // 错误:void* 不能进行指针运算
四.const 修饰指针
在C和C++中,
const
修饰符可以用来修饰指针及其指向的对象。这可以用来确保代码中的某些值不会被意外修改。
const
可以以几种不同的方式修饰指针
1.
const
修饰指针所指向的对象
当
const
修饰指针所指向的对象时(注意const在*的左边),表示通过该指针不能修改所指向的对象。这个声明可以解读为“指向
int
的指针是常量”。它意味着指针本身可以改变指向不同的地址,但不能通过该指针修改所指向的值。
int x = 10;
int y = 20;
const int* ptr = &x; // ptr 指向 x
ptr = &y; // 可以改变 ptr 的指向
// *ptr = 30; // 错误:不能通过 ptr 修改 y 的值
2.
const
修饰指针本身
当
const
修饰指针本身时(注意const在*的右边),表示指针本身是常量,不能指向其他地址。这个声明可以解读为“指针是一个常量,指向
int
”。它意味着指针必须在声明时初始化,之后不能改变其指向,但可以通过指针修改所指向的对象的值。
int x = 10;
int* const ptr = &x; // ptr 必须初始化
*ptr = 20; // 可以通过 ptr 修改 x 的值
// ptr = &y; // 错误:不能改变 ptr 的指向
3.
const
同时修饰指针和指针所指向的对象
当
const
同时修饰指针和指针所指向的对象时(注意int*两边都有const),表示指针和所指向的对象都不能被修改。这个声明可以解读为“指向
int
的常量指针是常量”。它意味着指针必须在声明时初始化,之后不能改变其指向,也不能通过该指针修改所指向的对象的值。
int x = 10;
const int* const ptr = &x; // ptr 必须初始化
// *ptr = 20; // 错误:不能通过 ptr 修改 x 的值
// ptr = &y; // 错误:不能改变 ptr 的指向
五.野指针
5.1野指针成应
(1). 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
(2).指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
(3).指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
5.2规避野指针
1.指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
初始化如下:
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
2**.小心**指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
3.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。
因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起 来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤。
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
4.避免返回局部变量的地址
#include<stdio.h>
int* test()
{
int a = 0;//局部变量a出了test函数就会被销毁
return &a;
}
int main()
{
int* p = test();
printf("%d\n",*p);
return 0;
}
因为出了test函数,局部变量a就已经被销毁了,本来属于局部变量a的地址,现在却已经不是他的了。此时这块地址的指向是不确定的
六.assert断言
assert
断言是一种用于在开发和调试阶段检测程序错误的工具。它在C和C++(以及其他编程语言如Python)中被广泛使用,以验证程序运行时的假设是否为真。如果断言失败,程序会中止执行,并通常会显示错误信息。
使用
assert
的步骤
- 包含头文件:- 在使用
assert
之前,需要包含头文件<assert.h>
。 - 使用
assert
宏:-assert
宏用于检查表达式是否为真。如果表达式为假,程序会终止并显示错误信息,包括表达式、文件名和行号。
基本运用:
#include <stdio.h>
#include <assert.h>
int main() {
int x = 5;
assert(x == 5); // 如果 x 不等于 5,程序将终止
printf("x is 5\n");
x = 10;
assert(x == 5); // 这一行将导致程序终止,因为 x 不等于 5
printf("This line will not be executed\n");
return 0;
}
检查指针是否为 NULL:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main() {
int* ptr = (int*)malloc(sizeof(int));
assert(ptr != NULL); // 检查内存分配是否成功
*ptr = 42;
printf("Value: %d\n", *ptr);
free(ptr);
ptr = NULL; // 释放内存并将指针置为 NULL
assert(ptr == NULL); // 检查指针是否为 NULL
return 0;
}
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。
完!
点个赞吧,感谢阅读!
版权归原作者 小容小容 所有, 如有侵权,请联系我们删除。