文章目录
一、内存和地址
首先我们需要先了解换算单位:
1 bit(比特)可以存放两个二进制数
1 Byte(字节)= 8 bit
1 KB = 1024 Byte
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB
1 PB = 1024 TB
…
二、指针变量和地址
1、取地址操作符 &
通过取地址操作符可以找到在内存单元的编号
int main()
{
int a = 6;
printf("%p\n", &a);
return 0;
}
2、指针变量和解引用操作符 *
那我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x0069FF40,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?
答案是:指针变量中。
int main()
{
int a = 6;
int * p = &a;
printf("%p\n", p);
return 0;
}
指针类型的确定主要看所指向的变量原本的类型,例:
char a = ‘h’
char * p = &a;
我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习一个操作符叫解引用操作符(*)。
int main()
{
int a = 6;
int* p = &a;
printf("%d\n", a);
*p = 0;
printf("%d\n", a);
return 0;
}
如上图所示,解引用操作符可以直接访问地址指向的空间,也可以改变空间所储存的数据。
3、指针变量的大小
指针变量的大小取决于地址的大小:
32位平台下地址是32个bit位(即4个字节)
64位平台下地址是64个bit位(即8个字节)
#include <stdio.h>
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
总结:首先我们需要了解指针变量是用来存放地址的,指针变量的大小取决于存放一个地址需要多大的空间。我们只需要区分32位平台的是由32位二进制所产生的地址,而64位的平台就是由64位二进制所产生的地址。
三、指针变量类型的意义
1、指针的解引用
//代码1
int main()
{
int n = 0x11223344; //0x表示一个十六进制数,其转化为十进制为287454020
int* pi = &n;
*pi = 0;
printf("%d\n", n); //结果为0
return 0;
}
//代码2
//注意:一个十六进制数占4个比特空间,2个十六进制数 = 8个比特 = 1个字节
int main()
{
int n = 0x11223344; //共有四个字节的空间
char* pc = (char*)&n;
*pc = 0;
printf("%d\n", n); //结果为287453952,改变第一个字节空间的数值44
return 0;
}
根本原因是char类型本身只占一个字节的空间,(char*)&n 可以理解成读取空间中第一个字节的空间的值,后续(*pc)解引用也只能读取n原本4个字节空间的第一个字节空间。
2、指针 ± 整数
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n", &n);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc + 1);
printf("pi = %p\n", pi);
printf("pi+1 = %p\n", pi + 1);
return 0;
}
运行结果:
我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
四、const 修饰指针
1、const 修饰变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量的也可以修改这个变量。但是如果我们希望一个变量加上一些限制,不能被修改,怎么做呢?这就是 const 的作用。
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
2、const 修饰指针变量
首先我们需要知道 const 在修饰指针有两种情况:
第一种限制 *p 的写法(口诀在 * 左边限制 *p)
int n = 10;
int m = 20;
//第一种写法:
const int * p = &n; //注意 const 的位置
//第二种写法:
int const * p = &n;//两次 const 的写法不同,但效果相同
*p = 30;//no
p = &m;//yes
第二种限制 p 的写法 (口诀在 * 右边限制 p)
int n = 10;
int m = 20;
int* const p = &n;//此时 const 在 p 左边
*p = 30; //yes
p = &m; //on
在实际过程中如果遇到都不想使其被修改的情况下:
int n = 10;
int m = 20;
int const* const p = &n;//出现了两次的 const
*p = 20; //on
p = &m; //on
五、指针运算
指针的基本运算有三种,分别是:
- 指针± 整数
- 指针-指针
- 指针的关系运算
1、指针 + 整数
上面已经提到指针的±运算,跟指针类型存在差异,从而根本导致运算的不同。
注意:数组的地址 == 数组的首项地址
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0]; //存放的是首项的地址,也就是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;
}
2、指针 - 指针
//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s; //*p 解引用的根本得到 s 的地址也就是'a'的地址
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));//默认最后一位是隐藏的'\0'
return 0;
}
结果:
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;
}
六、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1、野指针的成因
①指针未初始化
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
②指针越界访问
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i; //当i=11时,超过了数组的大小
}
return 0;
}
③指针指向的空间释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);//此时n在内存中已经被释放了
return 0;
}
2、如何规避野指针
①指针初始化
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;//当遇到不确定指针指向的方向时,可以先赋值位 NULL
return 0;
}
②预防指针越界访问
③使用后,不再使用时,应及时赋值位NULL,使用前也应当检查其有效性
④避免返回局部变量的地址
七、 assert 断言
assert.h 头⽂件定义了宏 assert() ,用于在运⾏时确保程序符合指定条件,如果不符合,就报
错终止运行。这个宏常常被称为“断⾔”。
assert(p != NULL);
上行代码的表达意思:当 p != NULL 时继续执行程序,否则终止运行,并且给出报错信息提示。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产是生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert() 的使用对程序员是非常友好的,使用 assert() 有几个好处:它不仅能⾃动标识⽂件和出问题的行号,还有⼀种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁用⽂件中所有的 assert() 语句。如果程序又出现问题,可以移
除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。
assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。
⼀般我们可以在debug中使用,在release版本中选择禁⽤assert就行,在VS这样的集成开发环境中,在release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在release版本不影响用户使用时程序的效率。
八、指针的使用和传址调用
1、传址调用
问题:交换 n 和 m 的值
void Swap(int x,int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 10;
int m = 20;
printf("n = %d m = %d \n",n,m);//打印后方便观察
Swap(n,m);
printf("n = %d m = %d \n",n,m);
return 0;
}
以上代码的打印结果会发现,没有任何改变,实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。所以Swap是失败的了。
我们现在要解决的就是当调用Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接
将a和b的值交换了。那么就可以使用指针了,在main函数中将a和b的地址传递给Swap函数,Swap
函数里边通过地址间接的操作main函数中的a和b就好了
void Swap(int* px,int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int n = 10;
int m = 20;
printf("n = %d m = %d \n", n, m);//打印后方便观察
Swap(&n, &m);
printf("n = %d m = %d \n", n, m);
return 0;
}
这里调用Swap函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。
2、 strlen的模拟实现
int my_strlen(const char * arr)
{
int count = 0;
while (*arr != '\0')
{
arr++;
count++;
}
return count;
}
int main()
{
int len = my_strlen("hello");
printf("%d\n", len);
return 0;
}