目录
1.内存与地址
内存
在讲解内存之前,先来一个生活中的小案例:
在我们上学的时候有一个叫宿舍的地方,想必大家都有过住宿舍的经历,想一想你是如何找到你所居住的宿舍的那?这时学校就会发给你,你的宿舍是几号楼和宿舍的门牌号。这时你就会精准的找到你的宿舍。
像这种方法也应用到了编译器当中,这里的内存就是以这种方法来进行编号的。在我们买电脑或手机的时候一般都有一个叫内存的东西一般有8G/16G/32G,这里的内存其实是指cpu可存储的数据。这些数据为了方便管理都有相应的编号。这里可以把整个宿舍区比作是内存,你要找一个数据就像是你要找宿舍具体的位置是一样的你只要知道了宿舍的一些编号就可以轻松的找到了,而这种编号在编译器中的名称就是地址。
地址
其实地址的还有另外一个名称就是指针。这里就有了一层关系:内存的编号==地址==指针
既然提到了,内存那就扩展一下:
计算机中常用的单位:
这是一些计算机中的单位以及之间的换算。从这里可以看出内存还是挺大的,所以需要编号来更好的确定数据的位置。还是以上面宿舍的例子来帮助我们更好的理解。其中,每个内存单元,相当于一个学生宿舍,一个字节有八个比特位就好比同学们住的八人间,每个人都是一个比特位。
如何理解编址
cup在访问内存中的某一个字节的空间时,必须要知道这个字节空间在内存中的位置,然而内存中的字节很多,所以需要给内存编址。这里的编址就相当于给内存编号。
地址总线中一般会用0或1的形式来显示一个内存中的地址。有点机器有32个地址总线那么它的地址就用32个0或1组成的数来表示。而数据总线是用来传输数据的控制总线是用来控制数据传输的方向的如果cup要读取内存的数据控制总线会产生一种R的信号来读取。如果cup要把数据传给内存控制总线会产生一种W的信号来传输地址。
2.指针变量和地址
指针中常用的操作符
取地址操作符(&)
我们知道内存中会进行编址,如果我们想要得到这个数据就要先知道它的地址。那这个数据的地址如何获取呢,这里就用到了取地址操作符。&这个操作符的用法就非常大重要了,下面给大家展示一下:
程序执行的结果就是a的地址。
解引用操作符(*)
我们通过取地址符号(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也需要储存起来,方便后期在使用,那我们就把这样的地址储存在指针变量中。解引用其实也很好理解,取地址(&)的作用是取出数据的地址,而解引用就是根据地址这个数值找到这个地址空间里面存放的数据。
如:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;//取出a的地址存放到指针变量p中
return 0;
}
这就是用指针变量的方式存放地址。
解引用的另一种使用方法是,可以改变原本地址中的数据:
这也是指针的主要神奇之处。
如何理解指针类型
如int*p这个指针变量的类型是int*。其实这里的int*中 * 的含义是指这是个指针,int的含义是指这个指针指向的类型是int。
指针的大小
我们已经知道了,其实指针就是地址。地址的大小是多少呢?其实在如何理解编址中也说明了地址是如何产生的就是根据地址总线的大小但在不同的环境下这个数值是有变化的。一般有两种一种是32位的机器,地址就是32个比特位换算为字节就是4(1字节=8比特位)。还有种是64位的机器,同理它就是8个字节。这里指针也是同理。
在编译器中一般有两种环境x64和x86。在x64的环境下是8,x86的环境下是4。原理和上述的一样只是名称的区别。
注意:指针的大小和指针类型是无关的,只要是指针大小就是4或8个字节。
指针变量类型的意义
既然指针变量的类型对指针的变量的大小无关,那设计这么多的指针变量类型有什么呢?
指针的解引用
下面看一个代码:
#include<stdio.h>
int main()
{
int n = 0x11223344;
//int* p = &n;
char*p=(char*)&n;
*p = 0;
return 0;
}
指针的类型决定了,指针解引用有多大的权限(一次可以操作几个字节);
如:调试我们可以看到int*的指针会改变四个字节,char*的指针会改变一个字节。
指针+-整数
#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;
}
运行结果:
这里就可以看到char*类型的指针+1跳一个字节,int*类型的指针+1跳四个字节。这就是指针类型不同带来的变化。
结论:指针类型决定了指针向前或向后走一步的大小。
void*指针
在指针中有一个特殊的指针类型void*类型。这种类型的指针可以接受任意的类型的地址。但是也有局限性,void*的指针不能进行解引用和指针整数+-的运算。
下面用代码来验证一下:
int main()
{
int n = 10;
int* p = &n;
char* pc = &n;
return 0;
}
如果你运行一下会报这样的错误:
int main()
{
int a = 10;
void* p = &a;
void* pc = &a;
*p = 0;
*pc = 9;
return 0;
}
如果你运行一下会报这样的错误:
那么void*类型的指针到第有什么用呢?
一般void*类型的使用在函数的参数部分,用来接收不同类型的数据地址。这里我们以后还会使用到。
const修饰指针
const修饰变量
变量是可以改变的,如果在变量的前面加const修饰一下就相当于给这个变量加了一个限制使得这个变量不可被修改。这就是const的作用。
int main()
{
int n = 0;
n = 10;//n可以修改
const int m = 0;
m = 10;//m不可以修改
return 0;
}
这里m还是一个变量,只是被const修饰。在语法上加了限制,如果修改就会出现语法错误。导致m不能直接被修改。
如果,我们绕过m,使用m的地址,去修改m就能做到了,虽然这样做是在打破语法规则。
这里我们可以看到被const修饰的变量还是被改变了,那么怎样让它更安全一点。这种打破const修饰的方法显然是不合理的。所以应该让p拿到n的地址也不能修改n,要怎么办呢?
const修饰指针变量
我们用代码来分析一下:
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;
p = &m;
}
void test2()
{
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;
const int*const p = &n;
*p = 20;
p = &m;
}
int main()
{
test1();//无const修饰
test2();//const在*的左边
test3();//const在*的右边
test4();//const在*的两边
return 0;
}
结论:const修饰变量的时候
1.const如果在*的左边,修饰是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针本身的内容会改变。
2.cosnt如果在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能改变,但指针变量所指向的内容,可以通过指针改变。
指针运算
指针+-整数
因为数组在内存中是连续存放的,只要知道第一个元素的地址,既可以找到后面所有的元素。
![](https://i-blog.csdnimg.cn/blog_migrate/b4bde2bf0544beb45ba9769fb1d8e740.png)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//p+i这里是指针+整数
}
return 0;
}
指针-指针
int my_strlen(char* s)
{
char* p = s;
while (*p != 0)
p++;
return p - s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
指针的关系运算
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)//指针的大小比较
{
printf("%d ", *p);
p++;
}
return 0;
}
野指针
这个野指针就是指指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)。
野指针的成因
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++) = i;
}
return 0;
}
3.指针指向的空间释放
#include<stdio.h>
int test()
{
int n = 10;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
变量n定义在函数test()中,然而函数在用完n这个变量时会把n变量的空间释放掉。
如何规避野指针
方法也很简单,只要不出现以上的几种情况就行了,除了要初始化和避免指针范围出界以外。
这里还有一个方法:指针变量不在使用时,即使置NULL,指针使用之前检测有效性
这里的NULL相当于专门约束指针的枷锁,只要指针被初始化为NULL它就不能被随意的使用。
如果指针不加以约束就会乱套,在写一些复杂的代码时需要用好多的指针,用NULL加以管理可以很好的提高效率。
这里还提到了指针的检测,那在使用指针之前要如何判断这个指针是否被初始化为NULL呢?
这里就要提到assert断言,这是一个库函数是专门检测指针是否为NULL的工具。
在用assert时要添加头文件<assert.h>,这个头文件里定义了assert的宏,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称之为”断言“。
assert(p!=NULL);
这个代码的运行就是检测变量p是否为NULL。如果确认不为NULL,程序继续运行,否则就会终止运行,并且给出报错信息的提示。这里会显示没有通过的表达式的文件名和行号。
这里给大家一段代码,体验一下asser()的奇妙之处:
#include<assert.h>
#include<stdio.h>
int main()
{
int* p = NULL;
assert(p != NULL);
return 0;
}
assert()对程序员是非常友好的,使用assert()有几个好处:它不仅能自动标识文件和出问题的行好,还有一种无需要更改的代码就能开启和关闭assert()机制。如果已经确认程序没有问题,不需要进行断言,就在#include<assert.h>前面定义一个宏NDEBUG
#define NDEBUG
#include<assert.h>
这样就禁止了所以的断言,如果要开启断言移除#define NDEBUG就可以。
assert()的缺点是,因为引入了额外的检测,增加了程序运行的时间,一般在Debug中使用,在使用Release版本中禁用assert就行。
指针的使用和传址调用
先来一段代码体验一下:
strlen的模拟实现
#include<stdio.h>
#include<assert.h>
size_t my_strlen(const char* str)
{
int count = 0;
assert(str);
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
传值调用和传址调用
例如:写一个函数,交换两个整型变量的值。
#include<stdio.h>
void swap1(int x,int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前: a=%d,b=%d\n", a, b);
swap1(a,b);
printf("交换前: a=%d,b=%d\n", a, b);
return 0;
}
运行结果:
我们发现结果并不是我们想的那样,这是我为什么呢?
在进行了调试以后你会发现其问题所在,这里就不展示过程了直接介绍原理:实参传递给形参的时候,形参会单独创造一份临时空间来接收实参,对形参的修改不会影响实参。说以swap1()是错误的。
swap1()的这种就是传值调用。
代码改进:
#include<stdio.h>
void swap2(int* px ,int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前: a=%d,b=%d\n", a, b);
swap2(&a,&b);
printf("交换前: a=%d,b=%d\n", a, b);
return 0;
}
这个代码运行一下就会发现这是正确的。
swap2()的这种方法就是传址调用。
所以未来函数中只是需要主调函数中的变量值来实现计算就用传值调用,如果函数内部要修改主调函数中的变量的值,就需要传值调用。