在讲内存和地址之前,我们想有个生活中的案例:
假设有一栋宿舍楼,把你放在楼里,楼上有100个房间,但是房间没有编号,你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,如:
有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
如果把上面的例子对照到计算中,又是怎么样呢?
其实也是把内存划分为一个个的内存单元,每个内存单元的大小取1个字节。
bit - 比特位
byte - 字节
KB
MB
GB
TB
PB
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间,比如:
int main()
{
int a = 10;
return 0;
}
0x005FFC54 0a
0x005FFC55 00
0x005FFC56 00
0x005FFC57 00
那我们如何能得到a的地址呢?
#include
int main() {
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
按照我画图的例子,会打印处理:005FFC54
那我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量
中。
比如:
int main() {
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
int*
,我们该如何理解指针的类型呢?int a = 10;
int* pa = &a;
int*
,*
是在说明pa是指针变量,而前面的int
是在说明pa指向的是整型(int)类型的对象。char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?
char ch = 'w';
char* pc = &ch;
我们将地址保存起来,未来是要使用的,那怎么使用呢?
在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去或者存放物品。
C语言中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符(*)。
#include
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
int main()
{
int num = 10;
int* p = #
char ch = 'w';
char* pc = &ch;
printf("%zd\n", sizeof(p));
printf("%zd\n", sizeof(pc));
return 0;
}
我们这里是x86环境下,猜猜这里这个p
和pc
的大小是多少?p是4个字节?pc是1个字节?
可以看到,都是4个字节,指针变量的大小是固定的,不要以为char*
类型的就小,看不起char*
类型的指针~~
x86环境下为什么
char*
的指针变量和int*
的指针变量都是4个字节呢?
指针变量是干什么呢?是为了存放地址的
那指针变量的大小是取决于存放一个地址需要多大的空间!!!
地址都是32个0/1组成的二进制序列的话,那么存放这个地址需要的空间的大小就是4个字节,所以指针变量的大小都是4个字节~~
同样x64环境,64根地址线,地址就是64个0/1组成的二进制序列,存放这样的地址,需要8个字节,所以指针变量的大小就是8个字节~~
如果不相信,我们在VS中试一下~~
#include
//指针变量的大小取决于地址的大小
//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;
}
指针
看扁了~~结论:
指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习~~
代码一:
#include
int main()
{
int n = 0x11223344;
int* pi = &n;
*pi = 0;
return 0;
}
代码二:
#include
int main()
{
int n = 0x11223344;
char* pc = &n;
*pc = 0;
return 0;
}
F10
,如果有的同学是笔记本,就在笔记本上按Fn+F10
,开始调试大小端存储
如果还有同学不知道的话可以看看这个章节->C生万物 | 深度挖掘数据在计算机内部的存储现在再来看第二个代码~~
char*
类型的变量里,那能不能放的下?*pc = 0
,我们这个是修改的几个字节?char*
的指针变量结论:
char*
类型的指针解引用访问1个字节,int*
类型的指针一次访问4个字节#include
int main()
{
int n = 0x11223344;
int* p = &n;
char* pc = &n;
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1);
printf("pc = %p\n", pc);
printf("pc + 1 = %p\n", pc + 1);
return 0;
}
char*
类型的指针变量+1
跳过1个字节, int*
类型的指针变量+1
跳过了4个字节。结论:
那有的同学会问,指针类型这些特点,怎么是使用呢?
我们先来回忆一下数组的方式~~
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//下标的方式
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
return 0;
}
我们再用指针的方式来访问~~
for
循环中*p
找到arr每个的元素,那找到一个元素,还想找下一个元素怎么办?那就要加1,因为p是整形指针,+1
跳过4个字节,正好找到下一个元素int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//下标的方式
int sz = sizeof(arr) / sizeof(arr[0]);
//int i = 0;
//for (i = 0; i < sz; i++) {
// printf("%d ", arr[i]);
//}
//指针的方式
int i = 0;
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d ", *p);
p = p + 1;
}
return 0;
}
p+1
,我直接*(p+i)
,这样可以吗?当然可以!!!int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//指针的方式
int i = 0;
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d ", *(p + i));
}
return 0;
}
相信看完了上面,你对指针类型有一个对应理解~~
#include
int main() {
int n = 100;
n = 200;
printf("%d\n", n);
return 0;
}
int main() {
const int n = 100;
n = 200;//err
printf("%d\n", n);
return 0;
}
int main() {
const int n = 100;
int* p = &n;
*p = 200;
printf("%d\n", n);
return 0;
}
n
要被const
修饰呢?就是为了不能被修改,如果p
拿到n
的地址就能修改n
,这样就打破了const
的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?const修饰指针有两种情况:
*
的左边*
的右边const
放在*
的左边~~int main() {
int m = 100;
int n = 10;
const int* p = &n;
*p = 0;
p = &m;
printf("%d\n", n);
return 0;
}
p
指向的值不能被修改const
放在*
的左边~~int main() {
int m = 100;
int n = 10;
int* const p = &n;
*p = 0;
p = &m;
printf("%d\n", n);
return 0;
}
结论:
cons
t如果放在*
的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。const
如果放在*
的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。int const * p = &n;
int *const p = &n;
const
放在*
左边还是右边int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
#include
//指针+- 整数
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;
}
p+i
就访问到每一个元素了~~int main() {
int arr[10] = { 0 };
int ret = &arr[9] - &arr[0];
printf("%d", ret);
return 0;
}
也可以这样理解,&arr[0]+9
—>>>&arr[9]
结论: 指针-指针得到的绝对值,是指针和指针之间元素的个数
strlen
吗?strlen
的功能是求字符串长度,如果有同学不了解这个函数的话可以去cplusplus网站上看一下int main()
{
char arr[] = "abcdef";
int len = strlen(arr);
printf("%d\n", len);
return 0;
}
\0
,让我求长度,我就统计\0
之前出现字符的个数my_strlen
,我们把数组传参,然后形参以指针接收,指针指向了数组首元素的地址,然后我们定义个计数器count,如果p!=\0
,count++
,p++
,最后返回count的个数~~int my_strlen(char* p)
{
int count = 0;
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
\0
为止,最后返回p-s就得到了元素的个数~~#include
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;
}
明白了上面的内容,我们再来将一个指针的关系运算~~
#include
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;
}
指针未初始化:
#include
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
cccccccc
不能!!!
指针越界访问:
#include
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;
}
我们这里的p++
是先执行的,而p++
是先使用后++
我们这里可以看到,我把arr[0]
的地址放入了*p
指针变量,然后我们进行遍历赋值,那我们这里判断条件是不是就越界访问了,超出了数组的范围,当指针指向的范围超出数组arr的范围时,p就是野指针,这是很危险的~~
我们这里还可以调试一下看看~~
arr
造到了破坏,而越界访问的内容也修改了,如此可见,这多么的危险!!!#include
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
test()
,返回一个地址,既然这个函数返回的是一个地址,那我用一个指针来接收,然后*p
这个地址n
在出这个函数的时候被销毁了,这就会造成指针指向的空间释放~~NULL
什么意思呢,我们用代码来说~~
int main()
{
int a = 10;
int* p = &a;
return 0;
}
ptr
,我现在不用,但我可以在后面才会用,但是这个指针变量不能空着,这个时候我们要给他初始化NULL
int main()
{
int a = 10;
int* p = &a;
int* ptr = NULL;
return 0;
}
那有的同学会说,那我直接给他赋值为0,可不可以呢?本质上是可以的,但是,当我们赋值为0了,有的时候会以为是一个整数,而我们赋值为NULL
的时候,那就很明显了,一看就是空~~
NULL
是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
这里我们也可以看到~~
NULL
NULL
int main()
{
int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
int* p = &arr[0];
for (int i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用的时候,判断p不为NULL的时候再使用
//...
p = &arr[0];//重新让p获得地址
if (p != NULL) //判断
{
//...
}
return 0;
}
test
函数,函数里创建了个数组,数组是局部变量,而我们返回了这个数组的地址,我们使用了一个指针变量p
来接收,当test
函数返回的时候局部变量已经被操作系统回收了,这就会造成野指针,如果有看过函数的栈帧的创建与销毁的话就明白了int* test()
{
//局部变量
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//.....
return arr;
}
int main()
{
int* p = test();//p就是野指针
return 0;
}
assert(p != NULL);
p
是否等于NULL
。如果确实不等于NULL ,程序继续运行,否则就会终止运行,并且给出报错信息提示。#include
int main()
{
int a = 10;
int* p = &a;
assert(p != NULL);
return 0;
}
#include
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);
return 0;
}
assert()
宏接受一个表达式作为参数。如果该表达式为真(返回值非零), assert()
不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert()
就会报错,在标准错误流stderr
中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。assert()
的使用对程序员是非常友好的,使用assert()
有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert() 的机制。如果已经确认程序没有问题,不需要再做断言,就在#include
语句的前面,定义一个宏NDEBUG
。#define NDEBUG
#include
然后,重新编译程序,编译器就会禁用文件中所有的assert()
语句。如果程序又出现问题,可以移除这条#define NDBUG
指令(或者把它注释掉),再次编译,这样就重新启用了assert()
语句。
assert()
的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在debug
中使用,在release
版本中选择禁用assert
就行,在VS这样的集成开发环境中,在release
版本中,直接就是优化掉了。这样在debug
版本写有利于程序员排查问题,在release
版本不影响用户使用时程序的效率。
一直叫传值调用,一种叫传址调用,接下来我们继续看~~
#include
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;
}
当我们运行代码,结果如下:
0x0039f9f0
,b的地址是0x0039f9e4
,在调用Swap1
函数时,将a和b传递给了Swap1
函数,在Swap1
函数内部创建了形参x和y接收a和b的值,但是x的地址是0x0039f90c
,y的地址是0x0039f910
,x和y确实接收到了a和b的值,不过x的地址和a的地址不一样,y的地址和b的地址不一样,相当于x和y是独立的空间,那么在Swap1
函数内部交换x和y的值,自然不会影响a和b,当Swap1
函数调用结束后回到main
函数,a和b的没法交换。Swap1
函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。结论: 实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。所以Swap是失败的了。
#include
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;
}
首先看输出结果:
my_strlen()
*str
的指针来接收,我们这个函数期望这个字符串来修改吗?不期望,这里我们再加上一个const
str
,确保指针的有效性str
指向a
的,当str!='\0'
,str++
,计数器也++,最后返回计数器~~size_t
计数器方式
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str);
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
好了,指针的第一部分就到这里就结束了~~
如果有什么问题可以私信我或者评论里交流~~
感谢大家的收看,希望我的文章可以帮助到正在阅读的你