1.内存和地址
1.1 内存
生活中我们有了房间号才能够快速找到房间,同样,在计算机中CPU(中央处理器)在处理数据时,需要的数据是在内存中进行读取的,处理完之后又会放回内存中。
在内存空间中,也是将内存划分为一个个的内存单元,每个内存单元的大小为1个字节,1个字节相当于8个比特位,一个比特位可以存储一个二进制的0或1
补充
1byte=8bit
1KB=1024byte
1MB=1024KB
1GB=1024MB
1TB=1024GB
1PB=1024TB
生活中我们将门牌号叫做地址,在计算机中,我们把内存单元的编号叫做地址,c语言中我们把它叫作指针
2.指针变量和地址
2.1
c语言中创建变量其实就是向内存中申请空间
我们使用一个获取地址的操作符(&)得到一个变量的地址
通过观察内存,&a取出的是a所占4个字节中较小的地址
2.2
2.2.1 指针变量
我们将取到的地址存放在指针变量中
int main()
{
int a=10;
int*p=&a;//取出a的地址存放在指针变量p中
return 0;
}
指针变量也是一种变量,这个变量就是用来存放地址的,存放在其中的值都可以理解为地址
2.2.2 拆解指针类型
pa左面写的是int*,*说明pa是指针变量,int说明pa指向的是整型类型的对象
2.2.3 解引用操作符(*)
int main()
{
int a=10;
int*pa=&a;//取出a的地址存放在指针变量p中
*pa= 0;
return 0;
}
通过解引用,*pa就把a由10 改为0
2.4 指针变量的大小
32位机器就有32根地址总线,把这32根地址总线产生的二进制序列当作一个地址,一个地址就是32byte,就是4个字节
在64位的机器中,就变成了8个字节
所以以后一提到地址的大小就是4个字节(32位)或者8个字节(64位)
x86环境就是32位环境,根据运行结果来看,指针变量的大小是与类型无关的,只要指针类型的变量在相同的平台下,大小就都是相同的
3.指针变量类型的意义
3.1 指针的解引用
指针类型决定了对指针解引用的时候有多大的权限(一次可以操作几个字节)
例如:char*的指针解引用只能访问一个字节,而int*的指针解引用就可以访问4个字节
3.2 指针+-整数
可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针跳过4个字节
指针的类型决定了指针向前走或向后走一步有多大
3.3 void*指针
void*是一种无类型的指针,可以用来接受任意类型的指针。局限性是,不能进行+-和解引用的运算
int main()
{
int a = 10;
int* p = &a;
char* pa = &a;
return 0;
}
运行后编译器给出警告
但是当我们用void*类型就不会出现这种问题
从这里看出,void*可以接收不同类型的指针,但是无法直接进行指针的运算
4.const修饰指针
4.1 const修饰变量
变量是可以被修改的,把变量的地址交给一个指针变量,通过这个指针变量也是可以修改变量的
我们希望加上一些限制,使其不能被修改,这就是const的作用
int main()
{
int m = 0;
m = 20;//m可以被修改
const int n = 0;
n = 20;
return 0;//n不能被修改
}
但是如果我们绕过你n,使用n的地址去修改n,就可以了,这样就是在打破语法规则
4.2 const修饰指针变量
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;//Ok n=20,m=20
p = &m;//N0,n=10,m=20
}
void test2()
{
int n = 10;
int m = 20;
int*const p = &n;//const修饰的是指针变量本身,指针变量本身不可以修改,但是指针变量所指向的内容可以改变
*p = 20;//OK
//p = &m;//这里编译器会报错
}
void test3()
{
int n = 10;
int m = 20;
int const* p = &n;//const修饰的是指针指向的内容,保证指针指向的内容不被改变,但是指针本身可以被改变
//*p = 20;//报错
p = &m;//OK
}
void test4()
{
int n = 10;
int m = 20;
int const* const p = &n;//const既修饰了指针变量本身,又修饰了指针所指向的内容,二者都不可以被改变
//*p = 20;//报错
//p=&m;//报错
}
int main()
{
test1();
test2();
test3();
test4();
return 0;
}
得出结论
1.const放在*的左边(int const *p):修饰的是指针所指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身可以改变
2.const放在*的右边(int *const p):修饰的是指针变量本身,保证了指针变量的内容不能改变,但是指针所指向的内容,可以通过指针改变
5.指针运算
指针的运算一般有三种..
1.指针+-整数
2.指针-指针
3.指针的关系运算
5.1指针+-整数=指针
数组在内存中是连续存放的,只要知道了第一个元素的地址,就可以找到后面所以的元素
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
这里的p+i就是指针+-整数
5.2 指针-指针=整数(两个指针之间的元素个数)
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;
}
//运行结果为3
指针-指针的运算的前提条件是:两个指针必须指向同一块空间
注意!!!指针+指针是无意义的
5.3 指针的关系运算
指针的大小比较
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
6.野指针
6.1 野指针的成因
1,指针未初始化
int main()
{
int* p;//局部变量未初始化,默认为随机值
*p = 20;
return 0;
}
2,指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
for (int i = 0; i < 11; i++)
{
*(p++) = i;//指针指向的范围超出数组arr的范围,p就是野指针
}
return 0;
}
3,指针指向的空间释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
在test()中,我们想让它返回一个指向局部变量n的指针。但是,存在一个问题,当test函数执行完毕是,n这个局部变量所占用的空间就会被释放,指向它的指针就会变为悬空指针。
在main()函数中,当我们想要打印*p是,实际上访问的是一个已经被释放的内存地址
6.2 如何避免野指针
6.2.1 如果明确知道指针指向哪里就直接赋地址,如果不知道,就给指针赋值NULL。
NULL是c语言中定义的一个标识符常量,值为0,0也是地址,但这个地址无法使用
初始化如下
int main()
{
int num = 10;
int* p1 = #
int* p2 = NULL;
return 0;
}
6.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围,超出了就是越界访问
6.2.3 指针变量不再使用时,即使设置为NULL,使用之前检查有效性
当指针变量指向一块区域时,我们可以通过指针访问该区域,后期不使用就即使设置为NULL,,只要是NULL指针就不去访问,在使用之前我们就要判断是不是NULL,如果是就不去访问
6.2.4 避免返回局部变量的地址
7.assert断言
assert.h头文件定义了assert(),用于在运行时确保程序符合指定的条件,如果不符合,就报错终止运行
assert(p!=NULL);
assert()接受一个表达式作为参数,如果表达式为真,程序继续运行,否则就会报错,并会在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及这个表达式的文件名和行号
如果确认程序没有任何问题的话,无需更改代码,在头文件前定义一个宏NDEBUG,编译器就会禁用所以assert()
它的缺点就是引入额外的检查,增加了程序的运行时间
我们可以在debug中使用,在release直接禁用就可
8.指针的使用和传址调用
8.1 strlen的模拟实现
strlen()的功能时求字符串的长度,统计“\0”之前的字符个数
函数原型为
size_t strlen(const char *P);
参考代码
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", len);
return 0;
}
8.2 传值调用和传址调用
写一个函数,交换两个整型变量的值,我们可能会写出以下代码
运行结果为
void Swap(int x, int y)
{
int temp = x;
x= y;
y= temp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
printf("%d %d", a, b);
return 0;
}
我们发现a和b并没有交换
我们在main函数内部,创建了a和b,在调用Swap函数时,将a和b传给了Swap函数,x和y接受了a和b的值,但是x和a的地址不同,是两块独立的空间,只在Swap函数内部交换x和y的值,自然不会影响a和b的值,所以a和b没有交换,这就是传值调用
结论:实参传递给形参时,形参会单独开辟一块空间接收,对形参的修改不会影响实参
我们要达到的目的时在Swap函数中操作的就是main函数中的a和b,所以我们把a和b的地址传给Swap函数就好了
void Swap(int *px, int*py)
{
int temp = *px;
*px= *py;
*py= temp;
}
int main()
{
int a = 10, b = 20;
Swap(&a, &b);
printf("%d %d", a, b);
return 0;
}
这种方式叫做传址调用