前几年做哪个网站致富,wordpress 展开折叠,app开发哪公司好,平台类网站开发要想学好C语言#xff0c;作为灵魂的指针那是必须要掌握的#xff0c;而要想搞定指针#xff0c;就不得不讲一下内存和地址之间的关系
内存和地址
计算机上的CPU#xff08;中央处理器#xff09;在处理数据的时候#xff0c;需要的数据是在内存中读取的#xff0c;处…要想学好C语言作为灵魂的指针那是必须要掌握的而要想搞定指针就不得不讲一下内存和地址之间的关系
内存和地址
计算机上的CPU中央处理器在处理数据的时候需要的数据是在内存中读取的处理后的数据也会放回内存中那么内存空间又是如何管理和使用的呢
其实内存空间也是划分为一个一个的内存单元的每个内存单元的大小取一个字节
补充计算机中的单位
计算机中的单位大致有以下几种
bit比特位、Byte字节、KB、MB、GB、TB、PB
他们的转换关系为
1Byte 8bit
1KB 1024Byte
1MB 1024KB
1GB 1024MB
1TB 1024GB
1PB 1024TB
其中一个比特位bit的大小是存放一个二进制位的0或1所占空间的大小。
每个内存单元的大小是1个字节即8个比特位而且每个内存单元都有一个编号有了这个编号CPU就可以快速的找到这个内存空间。 在计算机中我们把内存单元的编号也叫地址而在C语言中地址也叫指针。
如何编址
因为CPU访问内存的某块空间时是通过地址来完成的所以需要对内存进行编址。计算机中的编址是硬件在设计的时候就完成的跟钢琴的键位差不多所以并不是把每一个内存的地址都给记录下来。
那具体是如何编址的呢
首先补充一点CPU计算机内部是由很多硬件单元相互协同工作的而硬件与硬件又是相互独立的要把这些硬件相互联系起来就需要用线就是物理意义上的线连起来。那么内存和CPU之间也是通过这些线来完成大量的数据交互的硬件编址要用到其中一组线叫地址总线。
假设32位机器有32根地址总线每根地址总线都有两种状态表示0和1电脉冲的有无那么一根线就能表示两种含义两根就能表示4中含义32根地址线就能表示2^32中含义每一种含义代表一种地址。
地址具体如何使用
首先再补充两点
1CPU与内存或其他器件之间的数据传输是通过数据总线来进行的
2CPU对外部器件的控制是通过控制总线进行的
假设CPU要对内存进行一次读操作
首先控制总线先发一条读的命令R当地址总线传过来一个信息的时候把地址信息下达给内存在内存上就可以找到该地址对应的那块空间然后将里面的数据通过数据总线传入CPU内的寄存器这样就完成了一次读操作。
注找内存单元是地址来做的地址总线是用来传递地址信息的。
指针变量和地址
变量创建的本质
我们在写代码的时候会创建一些变量而创建变量的本质其实是向内存申请了一块空间
int n 5;
因为int类型的变量所占空间的大小是4个字节所以上面这句代码就是向内存申请了4个字节的空间存放5这个数字而这4个字节每一个都有一个地址
需要注意的是变量名其实是给我们看的编译器是通过地址来找内存单元的
取地址操作符
这个操作符可以取出变量在内存中的地址例如
#include stdio.h
int main()
{int n 8;int * p n;//p是指针变量类型是int*printf(%p\n, p);//%p是用来打印n的地址的return 0;
}
在这个代码中定义了一个int类型的变量n值是8n就可以得到变量n的地址需要注意的是一个int类型有4个字节每一个字节都有一个地址而n取出的是4个字节中地址较小的字节的地址这里用int*类型的指针变量p来存放n的地址因为在C语言中地址就是指针要用指针变量来接收至于为什么是int*类型下文讲指针变量类型的意义时会有解释
在int * p n;这句代码中p是指针变量的名字int *是指针变量p的类型*表示p是一个指针变量int表示p指向的变量n的类型是int 解引用操作符*
解引用操作符也叫间接访问操作符对指针变量进行解引用就能找到指针变量所指向的那块空间
#include stdio.h
int main()
{int n 50;int * p n;*p 70;printf(%d\n, n);return 0;
}
在这段代码中指针变量p中存放了n的地址然后*p 70;这句代码对p进行了解引用操作找到了变量n所在的空间并把n的值改变成了70最后打印n的值。 指针变量的大小
首先指针变量是用来存放地址的一个地址的存放需要多大空间那么指针变量的大小就是多大空间所以指针变量的大小与类型无关。
比如在32位机器上有32根地址总线每一根地址线上都会产生一个0或1的数字信号将这些数字信号组成的二进制序列作为一个地址就需要32个比特位bit即4个字节的空间才能存储。
同理在64位机器上有64根地址总线那么一个地址就需要64个比特位bit即8个字节。
指针变量类型的意义
指针的解引用
指针的类型决定了对指针进行解引用时候的权限即一次能访问几个字节比如指针类型是int*那么解引用操作就能访问4个字节的空间 如果指针类型是char*那么解引用操作就能访问一个字节的空间 指针加减整数
指针的类型决定了指针加减整数的时候向前或向后走一步有多大距离例如 p1是int*类型的指针1跳过一个整型即4个字节p2是char*类型的指针1跳过一个char类型的空间即一个字节
void*指针
void*类型的指针是无具体类型的指针也叫泛型指针所以它可以接收任意类型的地址但是void*类型的指针不能直接进行解引用操作和指针加减整数的操作
#include stdio.h
int main()
{int n 10;int* p1 n;void* p2 n;*p2 20;return 0;
}
编译结果如下指针变量的运算更其指向的对象无关而是取决于指针变量的类型的 从这段代码中可以看出
void*指针的作用
一般void*类型的指针是使用在函数的参数部分用来接收不同类型数据的地址这样的设计可以实现泛型编程的效果使得一个函数可以处理多种类型的数据
以后想使用的时候强制类型转换成想要的类型就行了
const修饰指针
const关键字
一个变量的值本来是可以被修改的要想变量的值不被修改就可以用const关键字
const修饰的变量具有常量的性质叫做常变量
这个被修饰的变量还是变量但是不能被修改例如
#include stdio.h
int main()
{const int n 7;n 24;//会出错return 0;
}
变量n被const修饰具有了常属性如果被修改就不符合语法规则编译时会报错
但是要修改变量n的值还有别的方法那就是指针
const修饰指针变量
先说结论
1const放在*的左边限制的是指针指向的内容即不能通过指针变量来改变它所指向的内容但是指针变量本身是可以修改的
int n 24;
int m 15;
int const *p n;//const int *p n;只要const在*的左边就行
p m;
上面这段代码中const限制的就是*p即*p不能被修改但p本身是可以修改的所以p m;这句代码就是对的
2const放在*的右边限制的是指针变量本身即指针变量不能改变它的指向但是可以通过指针变量修改它所指向的内容
int n 45;
int * const p n;
*p 56;
上面这段代码中const限制的就是p即p变量本身不能被修改但是*p是可以被修改的所以*p 56;这句代码就是对的
指针运算
指针加减整数
前面已经说过了指针变量类型的意义即指针的类型决定了指针解引用的权限也决定了指针加减整数的时候向前或向后走一步有多大距离例如用指针打印数组的元素
这里需要补充一些数组和指针的知识
数组名是数组首元素的地址比如一个整型数组arr那么arr就是数组首元素的地址而arr[0]也是数组首元素的地址因为是整型数组所以该地址的类型是int*
#include stdio.h
int main()
{int arr[] { 1,2,3,4,5,6,7,8,9,0 };int len sizeof(arr) / sizeof(arr[0]);int* parr arr;int i 0;for (i 0; i len; i){printf(%d , *(parr i));}return 0;
}
上面这段代码中将数组名即数组首元素的地址赋给了指针变量parr通过对指针变量的解引用操作来访问数组的每一个元素parr i就是下标为i的数组元素
运行结果 指针减指针
指针-指针的绝对值是指针和指针之间的元素个数但计算的前提条件是两个指针指向的是同一块空间例如
#include stdio.h
int main()
{int arr[] { 1,2,3,4,5,6,7,8,9,10 };int len sizeof(arr) / sizeof(arr[0]);printf(%d\n, arr[len - 1] - arr[0]);return 0;
}
这段代码中arr[len - 1]是数组的最后一个元素arr[0]是数组的首元素二者之间的元素个数如下图 运行结果 指针的关系运算
主要指比大小还以打印数组元素为例
#include stdio.h
int main()
{int arr[] { 1,2,3,4,5,6,7,8,9,10 };int len sizeof(arr) / sizeof(arr[0]);int* p arr;while (p arr[len - 1]){printf(%d , *p);p;}return 0;
}
p中存放着数组首元素的地址从p地址开始向后访问直到将数组所有元素全部打印完即到数组的最后一个元素为止
运行结果 野指针
野指针就是只想未知空间的指针这中指针是不安全的
野指针的成因
1指针未初始化
一个局部变量不初始化它的值是随机的那指针变量不初始化而是直接将指针变量的值当做地址解引用操作就会非法访问
2指针越界访问
3指针指向的空间释放如返回局部变量的地址
如何避免野指针
1指针初始化
一般情况下可以给指针变量赋某个变量的地址但如果不知道要给指针变量赋什么值可以给指针赋值为NULL
NULL是C语言中定义的一个标识符常量它的值是0因为0也是地址但是这个地址是无法使用的读写该地址会报错
2小心指针越界
3指针变量不在使用时及时置为NULL在指针使用之前检查指针的有效性在使用之前判断指针变量是否为NULL因为约定俗成的规则是只要是NULL指针就不去访问
4避免返回局部变量的地址
assert断言
assert断言常用来检测指针的有效性
assert.h头文件中定义了宏assert()用于在运行时确保程序符合指定条件如果不符合就报错终止运行这个宏常常被称作“断言”。
assert( p ! NULL );
当程序执行到上面这条语句时验证变量p是否等于NULL如果不等于NULL则程序继续执行否则会终止运行并给出报错信息提示比如下面这段代码
#include assert.h
int main()
{int* p NULL;assert(p ! NULL);return 0;
}
运行结果 在程序终止运行的同时给出了报错信息的提示 assert()宏接受一个表达式作为参数如果该表达式为真返回值非零assert()不会产生任何作用程序继续执行而如果该表达式为假返回值为零assert()就会报错在标准错误流stderr中写入一条错误信息显示没有通过的表达式以及包含这个表达式的文件名和行号
使用assert()的好处
assert()不仅能自动标识文件和出问题的行号还有一种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题不需要再做判断就在语句前面定义一个宏NDEBUG即
#define NDEBUG
#include assert.h
就像一个开关一样然后重新编译程序编译器就会禁用文件中所有的assert()语句
传值调用和传址调用
在函数传参的时候有时会传变量的值有时会传变量的地址
传值调用
当实参传递给形参的时候形参是实参的一份临时拷贝对形参的修改不会影响实参
传址调用
将实参的地址传递给形参形参通过地址可以找到地址所对应的变量传址调用可以让函数和主调函数之间建立真正的联系
指针的使用strlen函数的模拟实现
strlen是一个库函数作用是计算字符串中\0之前字符的个数函数原型如下
size_t strlen ( const char * str );
参数str接收一个字符串的起始地址然后开始统计字符串中 \0 之前的字符个数最终返回长度。如果要模拟实现可以从起始地址开始向后逐个字符的遍历只要不是 \0 字符计数器就1这样直 到 \0 就停止代码如下
//模拟实现库函数strlen
#include stdio.h
size_t mystrlen(const char* str)
{size_t count 0;while (*str){count;str;}return count;
}
int main()
{char arr[] abcdef;size_t len mystrlen(arr);printf(%zd\n, len);return 0;
}
在实现strlen函数的时候参数部分str指向了数组的第一个元素或者第一个字符并向后开始计算字符串的长度我们并不希望str指向的内容被修改所以用const对*srt进行了限制这样也提升了函数的健壮性也叫鲁棒性指的是在异常情况下系统生存的能力