【C语言复习】C语言中的数组与指针

写在前面

数组和指针小节,主要分为以下关键点:

  • 常见指针分类,如指针数组、数组指针、函数指针等。
  • 什么是数组/ 指针
  • 有关数组和指针的题目
  • 数组传参

我们也会围绕这些关键点来梳理复习。

数组和指针

指针基础概念

  1. 指针是一种特殊的变量,用来存放地址,这个地址可以唯一标识一块内存空间。
  2. 指针的大小固定,32位机器4字节,64位机器8字节。
  3. 指针有类型,指针的类型决定了指针加减整数要跳过多少字节。也会影响解引用时候的权限(可以在内存中取到多少位的内容)。
  4. 指针的运算。
    • 指针 ± 整数
    • 指针 - 指针
    • 指针的关系运算
  5. 野指针的成因和规避手段:
    1. 成因:
      • 指针没有初始化,默认为随机值
      • 指针越界访问
      • 指针指向的空间释放
    2. 规避手段
      • 指针创建时初始化
      • 小心指针的越界
      • 指针指向空间释放后立即置空指针
      • 避免返回局部变量的地址(局部变量出作用域销毁)
      • 指针使用之前检查有效性
  6. 指针和数组关系
    • 数组名表示的是数组首元素的地址(两个例外)
      • sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名代表整个数组。
      • &数组名,数组名代表整个数组。
    • 直接通过指针访问数组中的元素
  7. 二级指针:指针变量也是变量,也有地址,存放指针的地址的变量就是二级指针。

进阶知识

指针的分类

  1. 字符指针 char*,使用如下:
//usage 1:
int main()
{
	char ch = 'W';
	char * pc = &ch;
	*pc = 'w';
	return 0;
}
//usage 2:
const char* pstr = "hello world.";
printf("%s\n", pstr);
//put a string into a pointer

注意第二种,是把字符串的首字符放到了pstr中,通过首字符的地址就可以找到整个字符串。注意,如果用相同的常量字符串初始化不同的字符指针,指向相同的常量字符串。因为常量字符串在创建时被储存在常量区。指针指向同一个字符串时,实际指向相同的内存。但如果用相同常量去初始化数组,不同数组开辟出不同的内存块,储存在堆区。

注意第二种,是把字符串的首字符放到了pstr中,通过首字符的地址就可以找到整个字符串。注意,如果用相同的常量字符串初始化不同的字符指针,指向相同的常量字符串。因为常量字符串在创建时被储存在常量区。指针指向同一个字符串时,实际指向相同的内存。但如果用相同常量去初始化数组,不同数组开辟出不同的内存块,储存在堆区。

  1. 指针数组,存放指针的数组。
int* arr1[10]; //整型指针数组
char** arr2[10]; //二级字符指针的数组
  1. 数组指针,存放数组的指针。
int (*p)[10];
// *p -> int[10] 指向一个整型数组的指针,数组指针
char* (*pc)[20];
// *p -> char*[20] 指向一个字符指针数组的指针,数组指针

​ &数组名 和 数组名 的对比

&数组名 和 数组名,虽然打印出来的都是相同的地址,但是意义不同。

&数组名表示的是数组的地址,而数组名是代表数组首元素的地址。

&数组名的类型是: int(*)[10],是数组指针类型。 &数组名+1条过了整个数组。

数组名代表首元素的地址,但是二维数组的首元素地址实际上是第一行的地址,即一维数组的地址。所以如果传参传二维数组,可以传数组名,并以二维数组接收,也可以传递数组名用数组指针接收。

//二维数组
int arr[4][4] = {1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4};
//接收1
void print1(int arr[4][4],int row, int col);
//传参1
print1(arr,4,4);

//接收2
void print2(int(*arr)[4], int row, int col);
//传参2
print2(arr,4,4);
//小练习
int arr[5]; //数组
int *parr1[10]; //指针数组
int (*parr2)[10]; //数组指针
int(*parr3[10])[5]; // 5个(10个元素的指针数组)的指针 (数组指针)
  1. 数组和指针传参

    • 一维数组传参

      形参设置可以传指针,可以传数组

      void test(int* arr);
      void test(int arr[10]);
      
    • 二维数组传参

      形参设置可以传二维数组,可以传数组指针

      注意:传二维数组必须要知道一行多少元素,可以不知道有多少行。否则无法解析数组。

      //传二维数组
      void test(int arr[][5]);
      void test(int arr[3][5]);
      //传数组指针
      void test(int(*parr)[5]);
      
    • 一级指针传参

      一个函数的参数部分为一级指针时,函数可以接收同类型数组,以及一级指针。

    • 二级指针传参

      一个函数的参数部分为二级指针时,函数可以接收一个一级指针的指针(&一级指针),一级指针数组的数组名。

      void test(int** ptr);
      
      int main()
      {
      	int n = 100;
      	int * pn = &n;
      	int ** ppn = &pn;
      	
      	test(ppn); //传二级指针
      	test(&pn); //传一级指针的地址
      	
      	int* arr[10];
      	test(arr); //传一个一级指针数组的数组名
      }
      
  2. 函数指针

    void test();
    //对应的函数指针为
    void(*pfunc)();
    
  3. 函数指针数组

    把函数地址存放到一个数组中去,就是函数指针数组,定义方法是

    int (*parr1[10])();
    // parr1先与[]结合,代表是一个数组。
    // 数组的类型为 int(*)();
    

    函数指针数组作用有限,一个用途是转移表。

    #include <stdio.h>
    int add(int a, int b)
    {
    		return a + b; 
    }
    int sub(int a, int b)
    {
    		return a - b; 
    }
    int mul(int a, int b)
    {
    		return a*b; 
    }
    int div(int a, int b)
    {
    		return a / b; 
    }
    int main() {
        int x, y;
        int input = 1;
        int ret = 0;
        do{
    				printf( "*************************\n" );
    				printf( " 1:add 2:sub \n" ); 
    				printf( " 3:mul 4:div \n" ); 
    				printf( "*************************\n" ); 
    				printf( "请选择:" );
            scanf( "%d", &input);
            switch (input)
            {
    						case 1:
    								printf( "输入操作数:" );
                    scanf( "%d %d", &x, &y);
                    ret = add(x, y);
                  	printf( "ret = %d\n", ret);
                  	break;
                 case 2:
                 		printf( "输入操作数:" );
                    scanf( "%d %d", &x, &y);
                    ret = sub(x, y);            
                    printf( "ret = %d\n", ret);
                    break;
                  case 3:
                  		printf( "输入操作数:" );
                      scanf( "%d %d", &x, &y);
                      ret = mul(x, y);
                      printf( "ret = %d\n", ret);
                      break;
                  case 4:
                    	printf( "输入操作数:" );
                      scanf( "%d %d", &x, &y);
                      ret = div(x, y);
                      printf( "ret = %d\n", ret);
                      break;
                  case 0:
                    	printf("退出程序\n");
                      breark;
                  default:
                  		printf( "选择错误\n" );
                  		break; }
        } while (input);
        return 0;
    }
    

    如果使用函数指针数组实现,就简单很多。

    int main()
    {
        int x, y;
        int input = 1;
        int ret = 0;
        int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表 
        while (input)
        {
        	printf( "*************************\n" );
        	printf( " 1:add 2:sub \n" ); 
        	printf( " 3:mul 4:div \n" ); 
        	printf( "*************************\n" );
        	printf( "请选择:" );
          scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
          {
          		printf( "输入操作数:" ); 
          		scanf( "%d %d", &x, &y); 
          		ret = (*p[input])(x, y);
     	   	}
        	else
        			printf( "输入有误\n" ); 
        	printf( "ret = %d\n", ret);
        	}
        return 0;
    }
    

    可以发现简单很多,如果需要添加功能只需要改变数组中元素的个数,并将函数名填入其中即可。

  4. 指向函数指针数组的指针

    void gtest(const char* str)
    {
    		printf("%s\n", str);
    }
    int main()
    {
    		void(*pfun)(const char*) = test;
      	//函数指针
      	void(*pfunArr[5])(const char*);
      	pfunArr[0] = test;
      	//函数指针数组
      	void(*(*ppfunArr)[5])(const char*) = &pfunArr;
    		//定义函数指针数组的指针。
      	return 0;
      	
    }
    
  5. 回调函数

    回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址),作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。

    qsort就是c语言中比较常用的一个回调函数。用于数组的排序。

    void qsort (void* base, size_t num, size_t size,            int (*compar)(const void*,const void*));
    //参数含义:
    // base : 需要比较的数组名
    // num  : 数组中的元素个数
    // size : 数组中元素大小
    // compar : 调用者自己实现的一个函数,用于比较大小。
    

    举个例子使用它。

    void* compareFunc(const void* p1, const void* p2)
    {
    		return (*(int*)p1 - *(int*)p2);
    }
    int main()
    {
    		int arr[] = {1,3,4,5,9,2,7,0};
    		int i = 0;
    		qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), compareFunc);
    		for(int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i)
    		{
    				printf(" %d ", arr[i]);
    		}
    		return 0;
    }
    

指针和数组笔试题

  1. 强化训练数组名,&数组名等内容。
//一维数组
int a[] = {1,2,3,4}; 
printf("%d\n",sizeof(a));  
//16,sizeof数组名代表整个数组的大小
printf("%d\n",sizeof(a+0)); 
//4/8,a+0代表指针指向数组的第一个元素,仍是指针大小
printf("%d\n",sizeof(*a)); 
//4,既然不在两个例外,a就代表首元素,*a代表首元素地址解引用,就是首元素的大小,4
printf("%d\n",sizeof(a+1)); 
//4/8(32位/64位), sizeof(pointer)
printf("%d\n",sizeof(a[1])); 
//4,sizeof加数组中的一个元素等于sizeof(int)
printf("%d\n",sizeof(&a)); 
//a取地址,指针大小,4/8
printf("%d\n",sizeof(*&a));
//&a代表的是整个数组取地址,后面解引用,就是整个数组的大小16
printf("%d\n",sizeof(&a+1)); 
//&a代表整个数组取地址,+1跳过整个数组,但是还是指针的大小,4/8
printf("%d\n",sizeof(&a[0])); 
//&a[0]代表首元素取地址,指针大小,4/8
printf("%d\n",sizeof(&a[0]+1));
//&a[0]+1,代表首元素取地址然后+1跳过首元素,就是a[1]的地址,4/8
//字符数组
char arr[] = {'a','b','c','d','e','f'}; 
printf("%d\n", sizeof(arr)); 
//6,整个数组的大小
printf("%d\n", sizeof(arr+0)); 
//4/8,指针大小,指向第0个元素
printf("%d\n", sizeof(*arr)); 
//1,首元素的大小
printf("%d\n", sizeof(arr[1])); 
//1,第二个元素arr[1]的大小
printf("%d\n", sizeof(&arr)); 
//4/8,指针大小,代表整个数组的地址
printf("%d\n", sizeof(&arr+1)); 
//4/8指针大小,代表这个数组后面的地址
printf("%d\n", sizeof(&arr[0]+1));
//4/8指针大小,指向第0个元素之后
printf("%d\n", strlen(arr));
//大于等于6的随机值,没有'\0'。
printf("%d\n", strlen(arr+0));
//大于等于6的随机值,没有'\0'。
printf("%d\n", strlen(*arr));
//arr是首元素的地址,*arr就是首元素,a是97,会把97当成一个地址向后查找,可能会出现野指针问题。段错误
printf("%d\n", strlen(arr[1]));
//错误,arr[1]是第二个元素,b-98,错误
printf("%d\n", strlen(&arr));
//&arr代表整个数组的地址。随机值,和strlen(arr)相同
printf("%d\n", strlen(&arr+1));
//随机值
printf("%d\n", strlen(&arr[0]+1));
//随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//7,sizeof数组名代表整个数组的大小
printf("%d\n", sizeof(arr+0));
//arr+0 代表首元素的地址,指针大小4/8
printf("%d\n", sizeof(*arr));
//*arr代表首元素地址解引用,就是a的大小,1
printf("%d\n", sizeof(arr[1]));
//b的大小,1
printf("%d\n", sizeof(&arr));
//指针大小,4/8
printf("%d\n", sizeof(&arr+1));
//指针大小,4/8
printf("%d\n", sizeof(&arr[0]+1));
//指针大小,4/8
printf("%d\n", strlen(arr));
//6,""末尾有隐藏'\0'
printf("%d\n", strlen(arr+0));
//6
printf("%d\n", strlen(*arr));
//arr是首元素的地址,*arr就是首元素,a是97,会把97当成一个地址向后查找,可能会出现野指针问题。段错误
printf("%d\n", strlen(arr[1]));
//arr[1]代表b,第二个元素,把98当成一个地址向后查找,野指针。
printf("%d\n", strlen(&arr));
//&arr整个数组的地址,随机值,和strlen(arr)相同
printf("%d\n", strlen(&arr+1));
//随机值
printf("%d\n", strlen(&arr[0]+1));
//随机值

char *p = "abcdef";
printf("%d\n", sizeof(p));
//p是一个字符指针,指针大小4/8
printf("%d\n", sizeof(p+1));
//p类型是char*,p+1指针大小,4/8
printf("%d\n", sizeof(*p));
//*p是char类型,sizeof(char) = 1
printf("%d\n", sizeof(p[0]));
// sizeof(char) = 1
printf("%d\n", sizeof(&p));
//p是char*,&p是char**,指针大小4/8
printf("%d\n", sizeof(&p+1));
//&p+1,指针大小
printf("%d\n", sizeof(&p[0]+1));
//&p[0]就是char*类型,+1后,指针大小4/8
printf("%d\n", strlen(p));
//6
printf("%d\n", strlen(p+1));
//5
printf("%d\n", strlen(*p));
//经典野指针
printf("%d\n", strlen(p[0]));
//野指针,随机值
printf("%d\n", strlen(&p));
//随机值
printf("%d\n", strlen(&p+1));
//随机值
printf("%d\n", strlen(&p[0]+1));
//&p[0]是char*类型,等于5

//二维数组
int a[3][4] = {0}; 
printf("%d\n",sizeof(a)); 
//sizeof数组名为 整个数组的大小,是12*4 = 48
printf("%d\n",sizeof(a[0][0])); 
//一个int的大小,4
printf("%d\n",sizeof(a[0])); 
// a[0]单独存在,就是代表整个数组,在二维数组中,a[0]代表第一行,4*4 = 16
printf("%d\n",sizeof(a[0]+1)); 
// a[0]并非单独存在,代表首元素的地址,就是a[0][0]的地址,指针大小
printf("%d\n",sizeof(*(a[0]+1))); 
//a[0]+1,就是a[0][1]的地址,解引用后就是sizeof(int) = 4
printf("%d\n",sizeof(a+1)); 
//a并非单独存在,代表首元素地址,这里指首行的地址,加1仍为指针大小。
printf("%d\n",sizeof(*(a+1))); 
//a并非单独存在,代表首行的地址,+1代表第二行的地址,*后为16
printf("%d\n",sizeof(&a[0]+1)); 
//&a[0],代表第1行的地址,+1仍为指针大小
printf("%d\n",sizeof(*(&a[0]+1))); 
//代表第一行的地址,解引用后变为了16
printf("%d\n",sizeof(*a)); 
//a并非单独存在,代表首行地址,*后,16
printf("%d\n",sizeof(a[3]));
//a[3]是代表第4行的大小,总共四个元素,16
  1. 计算程序的运行结果:

    int main()
    {
    		int a[5] = {1,2,3,4,5};
    		int *ptr = (int*)(&a + 1);
    		printf("%d, %d", *(a + 1),*(ptr - 1));
    		return 0;
    }
    

    分析:

    a为一个5元素的数组,&a代表整个数组的地址,&a+1为跳过a数组的地址,强制转化为int*类型,ptr-1就指向a最后一个元素的地址。a+1代表着首元素后面一个元素的地址。所以结果为:2,5.在这里插入图片描述

  2. 计算程序的运行结果:

    struct Test{
    		int Num;
    		char* pcName;
    		short sDate;
    		char ch[2];
    		short sh[4];
    }*p;
    //假设结构体的大小为20个字节
    //假设p的值为Null(0x0).求下面表达式的值。
    int main()
    {
      	printf("%p\n", p + 0x1);
      	printf("%p\n", (unsigned long)p + 0x1);
      	printf("%p\n", (unsigned int*)p + 0x1);
      	return 0;
    }
    

    分析:

    p本身的类型为struct Test*,

    p + 0x1,向后跳过多少个字节,本身是根据p的类型决定的。p的类型是struct Test*,+1就是跳过一个 struct Test的大小。所以0x14

    (Unsigned long)p 类型是无符号long,其实就是把地址当作整数去加减,而不能用指针加减整数的规则。所以就是0x1

    (unsigned int*)p 类型是 unsigned int星,+1就是跳过一个unsigned int的大小,所以结果为0x4 。

  3. 计算程序的结果:

    int main()
    {
    		int a[4] = {1,2,3,4};
    		int* ptr1 = (int*)(&a + 1);
    		int* ptr2 = (int*)((int)a + 1);
    		printf("%x, %x", ptr1[-1], *ptr2);
    		return 0;
    }
    

    分析:

    a是一个int数组,&a代表整个数组的地址,+1代表指向数组之后的内存空间。用(int*)强转后,指向不变。ptr1[-1]就等价于 *(ptr1 - 1),可以发现就是4。

    ptr2略微麻烦一些。a强转成int后,+1就变成了直接加1,比如原来a的地址为0x0,(int)a + 1就是 0x1,相当于是在内存中往后面跳过了一个字节的大小,如图所示。(默认内存小端存储)在这里插入图片描述

  4. 计算程序的结果:

    int main()
    {
    		int a[3][2] = { (0,1), (2,3), (4,5) };
    		int* p = a[0];
    		printf("%d", p[0]);
    		return 0;
    }
    

    分析:

    a是一个二维数组,二维数组在内存中也是按照顺序连续排放的。

    但是,二维数组的定义方式是{ {0,1}, {2,3}, {4,5} }; 而不是小括号,而小括号的含义里面是 逗号表达式,(0,1) 是1,(2,3)是3,(4,5)是5。

    所以a这个三行二列的二维数组的真实组成实际上是:{1,3,5,0,0,0}

    a[0] 代表的是a[0]数组的首元素地址,p[0]代表的就是1.

  5. 计算程序的运行结果

    int main()
    {
    		int a[5][5];
    		int(*p)[4];
    		p = a;
    		printf("%p, %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    		return 0;
    }
    

    分析:

    int(*p)[4]; p = a;实际上就是把a看成是四列的数组去展开,图解这个题目。在这里插入图片描述

  6. int main()
    {
    	char* a[] = {"work", "at", "home"};
    	char** pa = a;
    	pa++;
    	printf("%s\n", *pa);
    	return 0;
    }
    

    分析:

    char* 的数组,里面有三个元素"work" “at” 和 “home”。pa是char**类型,pa++就是越过一个char星的大小,所以pa不再指向work,而是指向at。

  7. int main()
    {
    		char* c[] = {"ENTER", "NEW", "POINT", "FIRST"};
    		char** cp[] = { c+3, c+2, c+1, c};
    		char*** cpp = cp;
    		printf("%s\n", **++cpp);
        printf("%s\n", *--*++cpp+3);
        printf("%s\n", *cpp[-2]+3);
        printf("%s\n", cpp[-1][-1]+1);
    		return 0;
    }
    

    分析:

    解析如图。

    请添加图片描述

数组与指针小节完。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值