在讲内存和地址之前,我们想有个生活中的案例:
假设有一栋宿舍楼,把你放在楼里,楼上有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;
}

结论:
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。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,我现在不用,但我可以在后面才会用,但是这个指针变量不能空着,这个时候我们要给他初始化NULLint main()
{
int a = 10;
int* p = &a;
int* ptr = NULL;
return 0;
}

那有的同学会说,那我直接给他赋值为0,可不可以呢?本质上是可以的,但是,当我们赋值为0了,有的时候会以为是一个整数,而我们赋值为NULL的时候,那就很明显了,一看就是空~~
NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
这里我们也可以看到~~

NULLNULLint 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的指针来接收,我们这个函数期望这个字符串来修改吗?不期望,这里我们再加上一个conststr,确保指针的有效性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;
}
好了,指针的第一部分就到这里就结束了~~
如果有什么问题可以私信我或者评论里交流~~
感谢大家的收看,希望我的文章可以帮助到正在阅读的你🌹🌹🌹