目录
1.内存和地址
计算机中最小的单位是比特位bit,可以用来存储1个二进制位
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
内存单元编号 == 地址 == 指针
计算机中的编址,不是把每个字节的地址记录下来,而是通过硬件设计完成的。
地址总线:32位机器有32根地址总线,可以表示2^32种含义,每种含义表示一个地址。
2.指针变量和地址
2.1取地址操作符(&)
在C语言中创建变量的本质就是向内存申请空间。
%p可以用来打印地址
使用&取出地址(单目操作符)
&a取出的是a所占四个字节中较小的字节的地址
#include<stdio.h>
int main()
{
int a = 0; // 为a申请4个字节的空间
int *p = &a;
return 0;
}
2.2指针变量和解引用操作符
2.2.1指针变量
使用取地址操作符(&)取出的地址是一个数值,需要存储在指针变量中。
上述代码中,p是一个指针变量。指针变量也是变量,是用来存放地址的,存放在指针变量中的值会被理解为地址。
指针:地址
指针变量:存放地址的变量
口语中的指针一般指指针变量
2.2.2如何理解 int * p (如何拆解指针类型)
*说明p是一个指针变量
int说明p指向的对象是int类型
同样的,若p指向的类型是char类型,定义时要使用char*
2.2.3解引用操作符(*)
解作符(*)又叫间接访问操作符
*p表示通过p存放的地址找到p指向的对象
可以通过*p间接访问p指向的对象(例如可以通过*p=0将p指向的对象的值变为0)
*和&是可以相抵消的(*&a == a)
2.2.4指针变量的大小
32位机器假设有32根地址总线,每根地址总线的电信号转换成数字信号后都表示0或1,如果将32根地址总线产生的二进制序列当做一个地址,那么该地址就需要32个bit位进行存储,即32位机器的地址变量的大大小为4个字节。
同理,64位机器中的地址变量的大小为8个字节
指针变量的大小与其所指向的对象类型无关,只要是指针变量,在相同平台上的大小都是相同的。
如图,在x86(即32位)环境下,各种类型的指针变量大小均为4字节;在x64(即64位)环境下,均为8字节。
3.指针变量类型的意义
既然在同一个平台下各种类型的指针变量的大小是相同的,为什么还要有各种各样的指针类型呢?
因为指针类型决定了对指针进行解引用操作时有多大的权限。
比如:char*指针在解引用时只能访问一个字节,int*指针解引用时可以访问4个字节。
3.1指针+-整数
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = (char*) & a;
printf("pa = %p\n", pa);
printf("pa+1 = %p\n", pa+1);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc+1);
return 0;
}
运行结果
可以看出,当int*类型的pa进行+1操作时,地址由006FFC08变为006FFC0C,向后移动了4个字节(sizeof(int));而char*类型的pc+1时,由006FFC08变为006FFC09,向后移动了1个字节(sizeof(char))。这就是不同指针变量类型的差别。
结论:指针类型决定了指针向前或向后一步有多大(字节)。
3.2void*
在指针类型中有一种特殊类型是void*类型的,可以理解为无具体类型的指针(或泛型指针)。
这种指针可以用来接受各种类型的地址,但是也有局限性:void*类型的指针不能直接进行指针+-整数和解引用的运算。
一般情况下,void*类型的指针使用在函数参数的部分,用来接收不同数据类型的地址,可以实现泛型编程的效果,使一个函数可以处理多种类型的数据。
4.const修饰指针
4.1 const修饰变量
const int a = 10; // a具有了常属性(不能被修改)
在c语言中,虽然a不能被修改了,但本质上还是变量(常变量);在c++中,const修饰的变量就是常量
被const修饰的变量可以通过指针修改(破坏规则)
4.2 const修饰指针变量
//代码1
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
void test2()
{
//代码2
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//报错
p = &m; //ok?
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20; //ok?
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可以放在*的左边,也可以放在*的右边
当const放在*左边,修饰的是指针指向的内容,确保指针指向的内容不能通过指针改变,但指针变量内容本身可以改变。
当const放在*右边,修饰的是指针变量本身,保证了指针变量本身的内容不能修改,但是指针指向的内容可以通过指针改变。
也可以同时放在*左边和右边,表示指针指向的内容和指针变量本身均不能修改。
5.指针运算
指针运算有三种,分别是:
(1)指针+-指针
(2)指针-指针
(3)指针的关系运算
5.1指针+-指针
因为数组在内存中是连续存放的,所以只要知道第一个元素的地址,就可以找到后面所有元素。
Type * p;
p+1 --> 跳过 1*sizeof(type)
p+n --> 跳过 n*sizeof(type)
#include<stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
return 0;
}
以上代码可用于输出数组元素
5.2指针-指针
|指针-指针| = 两指针间的元素个数
前提:两指针指向同一块空间(如指向同一个数组)
不存在指针+指针(无意义)
5.3指针的关系运算
指针的关系运算实际上就是指针和指针比较大小
可以用来判断指针是否指向数组内、
6.野指针
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
6.1野指针成因
1.指针未初始化
指针变量未初始化,默认为随机值
2.指针越界访问
eg.当指针指向的范围超出数组范围时就成为野指针
3.指针指向的空间释放
eg.函数结束后指针指向的变量被释放,p成为野指针
6.2如何规避野指针
6.2.1指针初始化
指针初始化有两种情况:
1.明确知道指针应该指向哪里,初始化一个明确的地址
2.暂时不知道指针应该指向哪里,初始化为NULL
NULL是C语言中定义的标识符常量,值为0,0也是地址,这个地址是无法使用的,读写该地址会报错
6.2.2小心指针越界
一个程序向内存申请了多少空间,通过指针也就只能访问那些空间,不能超出访问范围,超出了就是越界访问。
6.2.3指针变量不再使用时及时置为NULL,指针使用前检查有效性。
不再使用某个指针时要将指针置为NULL,使用指针前先判断指针是否为NULL
6.2.4避免返回局部变量的地址
7.assert断言
assert.h头文件定义了宏assert(),用于在运行时确保特定程序符合指定条件,如果不符合,就报错终止运行,这个宏常被称为“断言”。
assert(p) //判断p是否为空指针,等价于assert(p!=NULL)
assert()宏接受一个表达式作为参数(任意表达式)。如果该表达式为真(返回值非零),assert()不会有任何反应,程序继续运行;如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示错误的表达式及其所在的文件名及行号。
使用assert()的好处:
1.可以自动标识文件和出问题的行号
2.如果已经确保程序没有问题,不再需要assert()断言,就在#include<assert.h>前定义一个宏NDEBUG。然后重新编译,编译器就会自动禁用文件中的所有assert()语句。
#define NDEBUG
#include<assert.h>
assert()的缺点:因为引入了外部的检查,增加了程序的运行时间。
在vs中,Release版本直接将assert()优化掉了,在linux版本中,Release版本仍会起作用。
8.传值调用和传址调用
传值调用:把变量本身传递给函数。实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不会影响实参。
传址调用:把变量的地址传递给函数。可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。
如果函数中只是需要主调函数中的变量值进行运算,可以采用传值调用;如果函数内部要修改主调函数变量的值,就要采用传址调用。
完