目录
[1.1 什么是内存](#1.1 什么是内存)
[1.2 编址](#1.2 编址)
[2. 指针的变量和地址](#2. 指针的变量和地址)
[2.1 取地址(&)](#2.1 取地址(&))
[2.2 指针变量](#2.2 指针变量)
[2.3 解引用](#2.3 解引用)
[2.4 指针变量大小](#2.4 指针变量大小)
[3. 指针变量类型存在的意义](#3. 指针变量类型存在的意义)
[3.1 不同类型指针的解引用](#3.1 不同类型指针的解引用)
[3.2 指针对整数的运算(+,-)](#3.2 指针对整数的运算(+,-))
[3.3 void* 指针](#3.3 void* 指针)
[4. const 修饰](#4. const 修饰)
[4.1 const 变量](#4.1 const 变量)
[4.2 const(指针变量)](#4.2 const(指针变量))
[5. 指针的运算](#5. 指针的运算)
[6. 野指针](#6. 野指针)
[7. assert断言](#7. assert断言)
[举例1:strlen 的模拟实现](#举例1:strlen 的模拟实现)
1.内存与地址
1.1 什么是内存
在这之前我们先说一下生活中的一些与内存非常像的例子
生活中有很多高楼,假设楼里有100个房间,你就住在其中一个,你的朋友要来找你,如果房间没有编号,那么就要一间一间去找,这样等你的朋友找到你,大概也天黑了,但是如果每个房间都有自己的编号,如:一楼:101,102....... 二楼:201,202.....以此类推,这样你的朋友就可以快速找到你。
其实计算机中的内存也是通过这样管理的,我们知道计算机CPU在处理数据的时候,需要的数据是在内存中读取的,用完了在放回内存中。
计算机也是把内存划分为一个个的内存单元,每个内存单元的大小是1字节。
补充:一个比特位可以储存一个2进制的位1或0。
bit- 比特位 1byte = 8bit
byte- 字节 1KB = 1024byte
KB 1MB = 1024KB
MB 1GB = 1024MB
GB 1TB = 1024GB
TB 1PB = 1024TB
PB
把这些每一个内存单元想成一个房间,一个房间可以放8个比特位。
把每个内存单元的编号想成一个内存的地址。
总结就是:内存单元的编号==地址==指针
1.2 编址
计算机中的编址,不是把每个字节的地址记录下来,而是通过硬件设计来完成的。就比如许多乐器的规则,都是设计者设计好的,所有人都遵循这个规则。其实本质上就是一直约定出来的共识。
2. 指针的变量和地址
2.1 取地址(&)
知道了内存和地址,其实在C语言中创建变量就是向内存申请一块空间。
向这样的地址a我们应该如何得呢?
这时就要用到我们的一个操作符(& )------取地址操作符
cs
#include <stdio.h>
//%p--打印地址
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
我们知道整型变量是占4个字节,这里我们只要知道第一个地址,就可以访问到4个字节的数据了
2.2 指针变量
当我们通过&得到的地址也是一个数(0x0023DF32),这个数值有时候也要储存起来,方便我们后面的使用。但是我们应该存放到哪呢?答案就是:指针变量。
cs
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;//把a的地址储存在p这个指针变量中。
return 0;
}
注意:指针变量也是变量,这种变量是用来存地址的,只要存放在指针中的值都会理解为地址。
指针的类型:
cs
int a=10;
int* p=&a;
这里的* 是在说明平时指针变量,而int 是在说明p指向的是整型类型的对象。
2.3 解引用
我们现在知道怎么把地址保证起来了,但是要怎么使用呢?在现实生活中,我们每个房间都会有一把打开它的钥匙,用了钥匙我们就可以打开房间,找到里面的东西。C语言其实也是一样的,我们拿到了地址,就可以通过地址找到地址指向的对象。这里就要用到解引用操作符 *()。
cs
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;//把a的地址储存在p这个指针变量中。
*p = 0;//解引用p,找到a并且将它的值改为0
printf("%d", *p);
return 0;
}
2.4 指针变量大小
cs
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(short*));
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(double*));
return 0;
}
3. 指针变量类型存在的意义
其实指针类型是有特殊意义的,不同的类型有不同的意义。
3.1 不同类型指针的解引用
我们先看两个程序:
cs
#include <stdio.h>
int main()
{
int n = 0x11223344;
int* pi = &n;
*pi = 0;
return 0;
}
cs
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
*pc = 0;
return 0;
}
通过调试我们发现第一个程序会把n的4个字节全部改为0,而第二个程序只会把第一个字节改为0
总结:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
3.2 指针对整数的运算(+,-)
我们可以看到,char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1,跳过4次字节。
总结:指针的类型决定了指针向前或者向后走一步的距离。
3.3 void* 指针
在指针类型中有void* 类型的,我们可以理解为没有具体类型的指针(泛指针),这种指针可以接收任意类型的地址。但是,void* 不能用来直接进行指针的加减整数和解引用运算。
注:⼀般 **void***类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。
4. const 修饰
4.1 const 变量
只要是变量,就是可以修改的。如果我们要想要这个变量成为不可变的,那么就要用到 const来修饰。
4.2 const(指针变量)
举例:
cs
#include <stdio.h>
//代码1
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;
p = &m;
}
void test2()
{
//代码2
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;
p = &m;
}
void test4()
{
int n = 10;
int m = 20;
int const* const p = &n;
*p = 20;
p = &m;
}
int main()
{
//测试⽆const修饰的情况
test1();
//测试const放在*的左边情况
test2();
//测试const放在*的右边情况
test3();
//测试*的左右两边都有const
test4();
return 0;
}
结论:const修饰指针变量的时候
1.const 如果放在*****的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
2.const 如果放在*****的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
5. 指针的运算
指针的基本运算有三种,分别是:
指针+- 整数------根据指针变量的类型,向前或向后移动
指针-指针
指针的关系运算
6. 野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)------没有约束的指针。
产生野指针的原因:
-
指针没有初始化
-
指针越界访问 ------只有10个元素,但是访问到第11个
-
指针指向的空间被释放------指针指向函数
避免野指针的方法:
- 指针初始化:指针一定要指向一个地址,没有就给指针赋值NULL(0)
2.小心指针越界:⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间。
-
指针变量不再使用时一定要及时置为NULL,指针使用之前检查有效性。
-
避免返回局部变量的地址
7. assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为"断⾔"。
cs
assert (p!=NULL)
//验证p是否等于NULL
//当p不等于NULL时,程序继续运行,否则停止,并且给出报错信息
assert 的实现:assert() 宏接受⼀个表达式作为参数。如果该表达式为真 (返回值⾮零),assert() 不会产⽣ 任何作⽤,程序继续运⾏ 。如果该表达式为假 (返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
使用的优点:可以自动标识文件和出问题的行号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。
补充:如果已经确认程序没有问题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 。
8.指针的使用和传址调用
举例1:strlen 的模拟实现
cs
#include<assert.h>
int my_strlen(const char* str)
{
int count = 0;
assert(str);//断言判的str是否为真(非0)
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
传址调用
例如:写⼀个函数,交换两个整型变量的值
我们一般的想法(传值调用):
注意:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
如果把要交换值的地址传给函数呢?
我们可以看到交换成功了。这种把变量的地址传递给了函数的方式叫:传址调用。
补充:传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。