指针

162 阅读13分钟

内存

  • 存储器:计算机的组成中,用来存储程序和数据,辅助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 &num;
}
int main(int argc, char const *argv[])
{
  int *p = getAddress();
  *p = 10;
}

  • 在函数getAddress()结束之后,空间会被释放,因此返回的是一个野指针,对野指针进行赋值到导致错误
#include<stdio.h>

int num = 0;
int * getAddress(){
  return &num;
}
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