内存
- 存储器:计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分
- 内存:内部存储器,暂存程序和数据,掉电会丢失数据SRAM,DRAM,DDR,DDR2,DDR3....
- 外存:外部存储器,长时间保存程序和数据,掉电不会丢失数据 ROM ,ERRROM,FLASH,硬盘,光盘....
内存是硬盘与CPU沟通的桥梁
- 暂存CPU中的运算数据
- 暂存硬盘等外部存储器交换的数据
物理存储器和存储地址空间
物理存储器:实际存在的具体的存储器芯片
- 主板上插入的内存条
- 显卡上显示的RAM芯片
- 各种适配卡上的RAM芯片和ROM芯片
存储地址空间: 对存储器编码的范围,通常所说的内存指的就是这个含义
- 编码:对每个物理存储单元(一个字节)分配一个号码
- 寻址“可以根据分配的号码找到对应的存储单元,完成数据的读写
内存地址
- 将内存抽象成一个很大的一维字符数组
- 编码就是对内存的每一个字节分配一个32位/64位的编码
- 这个内存编码就是内存地址
- 内存中每一个数据都会分配相应的地址
- char 占用一个字节 分配一个地址
- int 占用4个字节 分配四个地址
- ...
指针和指针变量
- 内存中的每一个字节都有一个编号,这个编号就是地址
- 如果在程序中定义了一个变量,在对程序进行编译或者运行时,系统就会给这个变量分配内存单元,并确认它的内存地址
- 指针的实质就是地址.指针就是地址,地址就是指针
- 指针是内存单元的编号,指针变量是存放地址的变量
- 通常在叙述时会把指针变量简称为指针,实际上所指的不一样
指针变量
- 指针是一种数据类型,指针变量是该类型的一种变量
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
int *p = &a;
printf("%p\n", &a); //0x7ffeeafd811c
printf("%p\n",p); //0x7ffeeafd811c
*p = 100;
printf("%d\n", a); // 100
printf("%d\n",*p); //100
return 0;
}
上面定义个int 变量a 系统给分配了内存,然后定义一个int类型的指针,指向了a的地址,因此两者的值是一样的
- int * 是指针数据类型,这个类型指向的是int类型的地址, p 是指针变量,p 本身就是地址因此 可以 p = &a 进行赋值 p 指向的是p所在地址的值,可以通过p 修改改地址上的值,也可以读取该地址上的值
指针的大小
- 使用sizeof()测量指针的大小,得到的总是4或者8
- sizeof()测的是指针变量指向存储地址的大小
- 在32平台,所有的指针都是32位的
- 在64平台,所有的指针都是64位的
指针的步长
- 不同类型的指针变量,所指向的内容的长度是不同的
- 比如 char *p 虽然指针占用了一个字节,但是 p所指向的地址 只占用了一个字节,所以p+1的话是增加了一个字节
#include<stdio.h>
int main(int argc, char const *argv[])
{
char b = 'a';
char *p = &b;
printf("%p\n",p);
printf("%p\n",p+1);
}
字符类型的指针 p+1就是 增加了一个字节 打印如下
0x7ffeef37411f
0x7ffeef374120
#include<stdio.h>
int main(int argc, char const *argv[])
{
int b = 10;
int *p = &b;
printf("%p\n",p);
printf("%p\n",p+1);
}
int 占用4个字节 因此 p+1 也是增加了4个字节 打印如下:
0x7ffeeea4911c
0x7ffeeea49120
#include<stdio.h>
int main(int argc, char const *argv[])
{
int **p;
printf("%p\n",p);
printf("%p\n",p+1);
}
指针变量p的类型是 int* ,它的长度相当于sizeof(int *),所以它的步长也是32位上是4 64位上是8
0x0
0x8
空指针和野指针
- 指针变量也是变量,变量就可以任意赋值,只要不越界即可
- 但是任意数值赋值给指针变量没有意义,因为任意赋值给指针变量所指向的区域是未知的,操作系统不允许操作此指针指向的内存区域,因此这样的指针就是野指针
- 野指针不会直接引发错误,操作野指针所指向的内存区域才会引发错误
- 但是野指针和有效指针变量保存都是数值,为了标志此指针变量没有指向任何变量,把NULL赋值给此指针,这样指针就是空指针,NULL是一个值为0的宏常量
万能指针
- void * 可以指向任意变量的内存空间
- 不可以定义void类型的变量,因为编译器不知道给变量分配多大的空间
- 但是可以定义void * 类型,因为这个指针在32位是4个字节,在64位上是8个字节
- void * 可以保存任意的地址,但是在操作该地址上的数据的时候,需要强转为指定的指针类型,因为在操作的时候不知道应该取几个字节的大小
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 65535;
void *p = &a;
char c = *(char*)p;
printf("%c\n",c);
}
相当于只取了一个字节的大小
const 修饰的指针
const int * p
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
const int *p = &b;
p = &a;
*p = 199; /// error *p 不可以被改变
}
- 这种情况 const 修饰的是*p,也就是p所指向的内容不可以修改
- p 的值可以被修改,可以指向任何 int *的地址
int * const p
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
int * const p = &b;
p = &a;/// error p的值不能被修改
*p = 199;
}
- 这种情况 const 修饰的是p,也就是p不能被修改,但是p所指向的值可以被修改
const int * const p
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
const int * const p = &b;
p = &a;/// error p的值不能被修改
*p = 199; // 值也不能被修改
}
- 这种情况 p所指向的地址和地址里面的值都不可以被修改
多级指针
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
int *p = &a;
int **q = &p;
printf("%p\n",&a);
printf("%p\n",p);
printf("%d\n",*p);
printf("%p\n",q);
printf("%p\n",*q);
}
打印结果
0x7ffeee62311c
0x7ffeee62311c
10
0x7ffeee623110
0x7ffeee62311c
- * 是取地址所指向的值,&是取该值所存放的地址,如果 *和&一起使用,两者相互抵消
- **q == *(*q) == *p == a
- **q == *(*q) == *p == *(&a) ==a
指针和数组
数组名
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a[] ={0};
int b = 10;
printf("%p\n",a);
printf("%p\n",&a);
a= &b;// error
}
- 数组名是一个数组的首地址,它是一个常量,不可以被更改
指针操作数据
步长
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a[10] ={1,2,3,4,5,6,7,8,9,10};
int * p = a;
printf("p = %p, *p = %d\n",p,*p);
printf("p+1 = %p,*(p+1) = %d\n",p+1,*(p+1));
}
打印结果:
p = 0x7ffeec403100, *p = 1
p+1 = 0x7ffeec403104,*(p+1) = 2
指针的步长就是数组中元素的sizeof,因此可以通过指针操作数组
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a[10];
int *p = a;
int len = sizeof(a)/sizeof(a[0]);
for(int i = 0;i<len;i++){
*(p+i) = len-i;
}
for(int i = 0;i<len;i++){
printf("a[%d]= %d,*(p+%d) = %d\n",i,a[i],i,*(p+i));
}
}
打印结果:
a[0]= 10,*(p+0) = 10
a[1]= 9,*(p+1) = 9
a[2]= 8,*(p+2) = 8
a[3]= 7,*(p+3) = 7
a[4]= 6,*(p+4) = 6
a[5]= 5,*(p+5) = 5
a[6]= 4,*(p+6) = 4
a[7]= 3,*(p+7) = 3
a[8]= 2,*(p+8) = 2
a[9]= 1,*(p+9) = 1
从结果上看通过数组名操作数据和通过指针操作数据是一样的
指针相减
通过两个类型一致的指针相减,可以得到中间跨过了多少元素,两个指针相加没有意义
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int * p =a; // p 指向了数组的首地址
printf("%p\n",a); // 打印数组的首地址
printf("%p\n",a+1); // a+1 表示首地址+1,指向的是a[1]的地址
printf("%p\n",a+9); // a[9]的地址
printf("%p\n",&a+1); //步长跨过了整个数组 相当于a[9]+1
printf("%p\n", (int *)(&a+1)-1); // 通过这种转换 相当于 a[9]的地址
int *q = (int *)(&a+1)-1; // 把a[9]的地址给了指针q
printf("%d\n", (int)(q-p)); // q-p 表示从第一个元素到最后一个元素跨过了 9个元素
}
指针数组
- 整型数组,是一个数组,数组的每一个元素都是整形
- 指针数组,也是一个数组,数组的每一个元素都是指针
#include<stdio.h>
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
int c = 30;
int * p[3] = {&a,&b,&c};
for(int i= 0;i<sizeof(p)/sizeof(p[0]);i++){
printf("%d\n",*p[i]);
}
}
指针与函数
- 指针作为函数的形参,可以改变实参的值
#include<stdio.h>
void swap(int* p,int *q){
int temp = *p;
*p = *q;
*q = temp;
}
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
printf("交换前: a = %d,b = %d\n",a,b);
swap(&a,&b);
printf("交换后: a = %d,b = %d\n",a,b);
}
数组作为函数的形式参数
- 数组作为函数的形参会退化为指针
#include<stdio.h>
void add(int *p,int *q,int len){
int temp = 0;
for(int i= 0;i<len;i++){
temp = *(p+i)+*(q+i);
*(p+i) = temp;
*(q+i) = temp;
}
}
int main(int argc, char const *argv[])
{
int a[10] = {0};
int b[10] = {1,2,3,4,5,6,7,8,9,10};
printf("相加前:\n");
for(int i=0; i<10; i++){
printf("a[%d] = %d,b[%d] = %d\n",i,a[i],i,b[i]);
}
add(a,b,10);
printf("相加后:\n");
for(int i=0; i<10; i++){
printf("a[%d] = %d,b[%d] = %d\n",i,a[i],i,b[i]);
}
}
指针作为函数的返回值
#include<stdio.h>
int * getAddress(){
int num = 10;
return #
}
int main(int argc, char const *argv[])
{
int *p = getAddress();
*p = 10;
}
- 在函数getAddress()结束之后,空间会被释放,因此返回的是一个野指针,对野指针进行赋值到导致错误
#include<stdio.h>
int num = 0;
int * getAddress(){
return #
}
int main(int argc, char const *argv[])
{
int *p = getAddress();
*p = 10;
}
- 在函数外边定义的变量是全局的变量,在整个工程中都可以使用,全局变量在程序启动的时候自动开辟空间,在程序解释的时候释放空间
指针和字符串
指针与字符数组
#include<stdio.h>
#include<string.h>
int main(int argc, char const *argv[])
{
char buf[] = "helloworld"; /// 定义一个字符数组,字符数组中的内容为helloworld\0
// 定义一个指针来保存数组元素的首地址
char *p = buf;
printf("%s\n",buf); // 打印helloworld
printf("%s\n",p); // 打印helloworld
printf("%s\n",p+2); // p是char 类型的指针 p+2相当于指针向后走了两步 打印的lloworld
printf("%c\n",*(p+4)); // 打印第五个字符 o
*p = 'm';
printf("%s\n",p); // 首地址的字符修改为m 打印melloworld
p++; // p指向了第二个字符
*p = 'a';
printf("%s\n",p); // 打印的结果是alloworld
printf("%d\n",strlen(buf)); // 10
printf("%d\n",strlen(p)); // 9
}
字符串常量
- "Hello" 如这样的事字符串常量
- 字符串常量是不可以改变的,存放在文字常量区
- 在使用的时候,双引号""代表取这个字符串首地址的地址
- char* p = "Hello";表示将字符串常量的地址赋值给指针p
字符指针作为形参
#include<stdio.h>
#include<string.h>
char* my_strcat(char* src,char* dest){
int len = strlen(src);
int index = 0;
while(*(dest+index)!=0){
*(src+len+index) = *(dest+index);
index++;
}
*(src+len+index) = 0;
return src;
}
int main(int argc, char const *argv[])
{
char src[128] = "hello";
char * dest = "world";
my_strcat(src,dest);
printf("%s\n",src);
}
字符串处理函数
strcpy
char * strcpy(char* dest,const char * src)
- 功能: 把src所指向的字符串复制到dest所指向的空间中,'\0'也会被拷贝进去
- 成功返回字符串的首地址,失败会返回null
- 如果参数dest所指向的内存空间不够大,可能会造成缓冲溢出的错误情况
void testStrcpy1(){
char * src = "Hello 1\0 world";
char dest[128] = "Welcome";
char* result = strcpy(dest, src);
printf("%s\n",result);
}
把src所指向的字符串复制到dest所指向的空间,到'\0'结束,'\0'也会被copy 进去,覆盖了dest的内容
void testStrcpy2(){
char * src = "Hello 1\0 world";
char dest[3] = "ab";
char* result = strcpy(dest, src);
printf("%s\n",result);
}
- dest 的空间不足以复制src的字符串,报了错误
strncpy
char * strcpy(char* dest,const char * src,size_t n)
- 把src指向的字符串的前n个字符复制给dest所指向的空间中,是否copy结束,看指定的长度是否包含'\0'
void testStrcpy1(){
char * src = "Hello 1\0 world";
char dest[128] = "Welcome";
char* result = strncpy(dest, src,3);
printf("%s\n",result); /// 打印结果是Helcome
}
- 当copy指定个字符的时候会把前面的字符给覆盖掉
- 如果copy的长度超过了dest的长度,一样会报错
strcat
char * strcat(char * dest,const char *str)
- 将str 字符串 连接到dest的尾部
- strncat 是 连接n个指定的字符到dest后面
- 如果最终的长度超过了dest的长度,和strcpy一样也会报错
- 如果遇到'\0'会自动结束
- 如果指定n个字符,在结尾的时候会自动添加上'\0'
int strcmp(const char *s1,const char *s2)
int strncmp(const char *s1,const char *s2,size_t n)
- 比较两个字符串的大小,是一个一个字符比较的ASCII的大小
- 比较的时候遇到'\0'就结束了
- strncmp 是比较的前n个字符
sprinft 组包函数
int sprintf(char * str,const char * format,...)
- 根据参数format 字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到再出现'\0'
- 参数:
- str 字符串首地址
- format 字符串格式,用法和printf 一样
- 返回值:
- 成功: 实际格式化的字符个数
- 失败 -1
void testSprintf(){
int year = 2023;
int month = 4;
int day = 9;
char buf[1024] = "";
int len = sprintf(buf, "%d-%d-%d",year,month,day);
printf("长度 = %d\n",len); // 8
printf("%s\n",buf); // 2023-4-9
}
sscanf 拆包函数
int sscanf(const char * str,const char * format,...)
- 从str 指定的字符串读取数据,并根据参数format 字符串来转换并格式化数据,
- 返回值
- 成功:参数树木,成功转换的值的个数
- 失败:-1
void testSscanf(){
int year = 0;
int month = 0;
int day = 0;
char * p = "Hello,2012-12-13";
int len = sscanf(p,"Hello,%d-%d-%d",&year,&month,&day);
printf("len = %d,year = %d,month = %d,day = %d",len,year,month,day); //len = 3,year = 2012,month = 12,day = 13
}
strchr
char * strchr(const char* s,int c)
- 在字符串s中查找字符c出现的位置
- 返回值:
- 成功 返回第一次出现的c地址
- 失败 NULL
void testStrchr(){
char * s = "Hello world";
char * result = strchr(s,'l');
printf("result = %s",result); /// llo world
}
strstr
char * strstr(constr char* haystack,const char * needle)
-
在字符串haystack中查找字符串needle出现的位置
-
参数:
- haystack:源字符串首地址
- needle 匹配字符串首地址
-
返回值:
- 成功返回第一次出现的needle地址
- 失败:返回NULL
strtok
char * strtok(char * str,const char * delim)
- 来将字符串分割成一个个片段,当strtok在参数str的字符串中发现参数delim中包含的分割字符时,则会将该字符改为\0字符,当连续出现多个时,只替换第一个为\0
- 参数:
- str:指向将要分割的字符串
- delim:为分割字符串中包含的所有字符
- 返回值:
- 成功:分割后字符串首地址
- 失败 返回NULL
void testStrtok(){
char buf[] = "abd*bsdd*sahdj*sjaljd*";
char *s = strtok(buf,"*");
while(s!=NULL){
printf("s = %s\n",s);
s = strtok(NULL,"*");
}
注意:
- buf 需要是一个可变的指针
- 第一次调用时,strtok 必须给予参数str字符串
- 往后的调用则将参数str设置为null,每次调用成功则返回只想被分割出来片段的指针
atoi
int atoi(const char * nptr)
- atoi 会扫描nptr字符串,跳过前面的空格字符,直到遇到数字或者正负号才开始做转换,而遇到非数字或者字符串结束符号‘\0’ 才结束转换,并将结果返回
- 返回值:成功转换后的整数
- 类似的函数有:atof atol