目录
一.内存和地址
1.1内存
CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
计算机把内存划分为一个个的内存单元,每个单元的大小取一个字节。
计算的常见单位:
bit - 比特位 | |
byte - 字节 | 1byte = 8bit |
KB | 1KB = 1024byte |
MB | 1MB = 1024KB |
GB | 1GB = 1024MB |
TB | 1TB = 1024GB |
PB | 1PB = 1024TB |
每一个比特位可以存放一个2进制的数字0或者1。
一个字节空间可以存放8个比特位。
为了方便CPU快速寻找到每一个空间,每个内存单元都有一个编号。
在计算机中我们把内存单元的编号称为地址。
C语言中我们给地址起了一个新名字:指针。
所以我们可以理解为:内存单元的编号==地址==指针。
1.2理解编址
二.指针变量和地址
2.1取地址操作符
在C语言中,创建变量其实就是向内存申请空间
比如:
上述代码中创建了变量a,内存中申请了4个字节,用于存放整数10,其中每个字节都是有地址的,如图中的内存窗口。
为了得到a的地址,我们就要用到操作符(&)————取地址操作符。
我们很容易发现&a取出的是a所占字节中较小的字节的地址。
虽然整形变量占了四个字节,但是我们只需知道第一个字节,就可以找到四个字节的数据。
2.2指针变量和解引用操作符(*)
2.2.1指针变量
我们通过取地址操作符(&)拿到的是一个数值,这个数值有时候也是需要储存起来的,方便我们使用,那我们把这样的地址存放在指针变量中。
比如:
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
return 0;
}
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量的值都会理解为地址。
2.2.2指针类型
我们看到的pa的类型是int*。
*是在说明pa是指针变量,而int 在说明pa指向的是整形类型的的对象。
例如:
//char类型的变量ch,ch的地址
char ch = 'a';
char* pch = &ch;
2.2.3解引用操作符
我们将地址保存起来,是为了未来使用的。
在C语言中,我们拿到地址(指针),就可以通过地址(指针)找到地址(指针)所指向的对象,这时候我们就需要用到操作符---解引用操作符(*)。
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
*pa = 0;
printf("%d\n",a);
return 0;
}
2.3指针变量的大小
32位的机器有32根地址总线,每根地址总线的出来的电信号转换成数字信号后是1或者0,那我们把32根地址总线的产生2进制的序列当作一个地址,那么一个地址就是32位比特位,需要4个字节才能存放。即指针变量的大小是4个字节
同理如果是64位的机器,指针变量的大小就是8个字节。
#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;
}
在X86的环境小输出的结果就是
4
4
4
4
在X64的环境下输出的结果就是
8
8
8
8
注意:指针变量的大小与类型是无关的,只要是指针类型的变量。在相同平台下,大小就是相同的。
三.指针变量类型的意义
3.1指针的解引用
在上面两个代码中我们可以看到,代码1会将n的四个字节全部改为0,但是代码2只是将n的第一个字节改为0。
指针的类型决定了,对指针解引用的时候有多大的权限(一次可以操作几个字节)
如:char*的指针解引用就只能访问1个字节,而int*的指针解引用就能访问四个字节
3.2指针+-整数
观察下面这段代码
#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跳过的是4个字节
即指针的类型决定了指针加一减一向前或者向后走一步的距离有多大。
3.3void*指针
在指针类型中,有一种特殊的类型是void* 类型的指针,可以理解为无具体类型的指针(泛型指针),这种类型的指针可以接受任何类型的地址。但是也有局限,void*类型的指针不能直接进行指针的加减整数和解引用的运算。
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上面的代码中,将int类型的变量的地址赋值给char*类型的指针变量,编译器会报错。
但是使用void*类型的指针就不会。
一般void*类型的指针是使用函数的参数的部分,用来接受不同数据类型的地址。这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。
四.const修饰指针
4.1const修饰变量
当用const修饰变量的时候,变量的值是不能被修改的,比如下面的代码
#include<stdio.h>
int main()
{
int n = 10;
const int m = 10;
return 0;
}
n的值是可以改变的,而const的值是不能改变的。
但是我们可以绕过n,使用n的地址,去修改n,就可以做到修改n的值。
#include<stdio.h>
int main()
{
const int n = 10;
printf("n = %d\n",n);
int* p = &n;
*p = 20;
printf("n = %d\n",n);
return 0;
}
这样我们就将n的值从10变成了20;
4.2const修饰指针变量
观察下面四个代码
#include <stdio.h>
//代码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;//ok?
p = &m; //ok?
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
void test4()
{
int n = 10;
int m = 20;
int const* const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
int main()
{
//测试⽆const修饰的情况
test1();
//测试const放在*的左边情况
test2();
//测试const放在*的右边情况
test3();
//测试*的左右两边都有const
test4();
return 0;
}
test2 和3和 4()会报错。
结论const在修饰指针变量的时候,
如果const放在*的左边,修饰的是指针说指向的内容,确保指针所指向的内容不能被修改。但是指针变量可以改变。
如果const放在*的右边,修饰的是指针变量本身,确保指针变量的内容是不能被修改的,但是指针所指向的内容,可以通过指针进行修改。
五.指针运算
指针运算有三种:
1.指针+-整数
我们知道数组在内存中是连续存放的,我们只要知道一个元素的地址,就能找到数组中的所有元素。
#include <stdio.h>
//指针+- 整数
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是下标为0的数组元素的地址,p+i就是下标为i的元素的地址,再解引用就可以知道数组的元素是什么了
我们得到了两个等式
数组名就是首元素的地址
p=arr;
arr[i] = *(p + i) = *(i + p) = i[arr];
我们可以进行适当验证!
当然不建议这样写!
2.指针-指针
#include<stdio.h>
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;
}
p-s就是指针减指针,算出来的是字符串长度。
3.指针的关系运算
//指针的关系运算
#include <stdio.h>
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;
}
六.野指针
野指针就是指向的位置是不可知的,(随机的,不正确的)。
6.1造成野指针的三种原因
1.指针为初始化
#include<stdio.h>
int main()
{
int* p;
*p = 10;
return 0;
}
此时的局部变量未初始化,默认为随机值。
2.指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
*(p++) = i;
}
return 0;
}
当指针指向的范围超出的数组arr的范围时,p就变成了野指针。
3.指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
由于局部变量存在生命周期,虽然我们可以得到输出结果是100,但是n作为局部变量已经被销毁了,此时就是指针指向的空间释放了
6.2规避野指针
6.2.1指针初始化
如果我们明确知道指针指向的哪里就直接赋值地址,如果不知道指针应该指向哪里,我们就可以将指针赋值为NULL.
NULL是C语言中定义的一个常识标识符常量,值为0,0也是地址,这个地址是无法使用的,读写这个地址会报错。
#include <stdio.h>
int main()
{
int num = 10;
int* p1 = #
int* p2 = NULL;
return 0;
}
6.2.2避免指针越界
一个程序向内存申请了空间,通过指针也就只能访问固定的空间,不能超出访问范围,超出了就是越界访问。
6.2.3及时置为NULL
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不问,
同时使⽤指针之前可以判断指针是否为NULL。我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。
6.2.4避免返回局部变量的地址
比如造成野指针的第三个例子
七.assert断⾔
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。assert() 的使⽤对程序员是⾮常友好的,使用assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h>语句的前⾯,定义⼀个宏 NDEBUG 。然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。
八.指针的使⽤和传址调⽤
8.1strlen
库函数strlen的作用是求字符串的长度,统计字符串中\0之前的字符的个数。
int 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;
}
8.2传址调用
#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;
}
如果我们要调换a和b的值,我们观察上面的代码,会发现输出结果没变
因为形式参数是实际参数的一份临时拷贝,形参的改变不会改变实际参数的值。
这种Swap1函数的使用是传值调用,所有我们不能实现这样的效果!
而传址调用可以
比如:
#include <stdio.h>
void Swap2(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 10;
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;
}