C基础(四)指针的使用

一、一级指针

指针也是一个变量,指针存放的内容是一个内存地址,该地址指向一块内存空间。

1.1 指针变量的定义

一级指针变量的数据类型会在基本数据了类型之后多了一个*号,指针变量只能存放内存地址(一个16进制的数),不能将一个基本数据类型直接赋值给一个指针变量。

如果要取出一级指针变量指向的内存地址所对应的值的话,可以通过在指针变量前加一个*号来获取

int *p;//表示定义一个指针变量p, 类型是int *;
*p;//代表获取该指针指变量指向的内存块对应的实际数据

int *p = 100; //错误, 只能指向内存地址, 是一个16进制的数
*p =100 ;  //正确, 因为*p操作的是变量的值.

1.2 &取地址运算符

通过&符号可以取得一个变量在内存当中的地址,然后就可以赋值给指针变量了。

#include<stdio.h>

int main()
{
	int a = 100;
	int *p;// 定义了一个int *类型的指针变量,名字叫做p
	p = &a;// 给指针变量赋值,把a的内存地址赋值给指针变量p
	*p = 200;//修改a的值,效果等于直接给a赋值, *p代表指针指向变量的值,而p代表指向变量的内存地址
	printf("p=%p,*p=%d,a=%d\n",p,*p,a); 

    // 通常为了书写方便,可以直接这样给指针变量赋值
    // int *p1 = &a;
    
    // 直接修改a变量的值,内存地址并不会修改
    a = 50;
    // 查看*p的值
    printf("p=%p,*p=%d,a=%d\n",p,*p,a);

	return 0;
}

两次输出结果如下:
在这里插入图片描述
可以看到*p和a变量的值是一样的,并且修改值并不会影响p指针变量指向的内存地址。

1.3 无类型指针

定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void *转化为其他类型指针,也可以用(void *)将其他类型指针强制转化为void类型指针。
void *p 他可以指向任意类型的内存地址

1.4 空指针与野指针

NULL在C语言中的定义为(void *)0 ,它是一个宏常量, #define NULL 0 .如果一个指针变量没有明确指向一块内存地址, 那么就把这个变量指向NULL,这个指针就是空指针,空指针是合法的,例如:

int *p = NULL;
// 或者
int *p;
p = NULL;

指向NULL的指针叫空指针,没有具体指向任何变量地址的指针(也就是没有初始化值的指针)叫野指针。

#include<stdio.h>

int main()
{
	// 定义了一个int *类型的指针变量p
	int *p;  
	// 如果指针变量没有初始化就使用,那就是野指针,
	// 由于它不知道指向的内存地址是啥,所以无法修改这块内存地址所对应的数值, 程序执行的时候会报错
	*p = 100; 

	return 0 ;
}

1.5 指针的兼容性

指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double *赋值给int * ,它是不会自动类型转换的.
原则上一定是相同类型的指针指向相同类型的变量地址,不能用一种类型的指针指向另一种类型的变量地址。

1.6 常量指针与指针常量

1、指向常量的指针(常量指针)
常量指针的const书写在指针类型之前,例如:
const char *p;
定义一个指向常量的指针, 但是这个指针不能通过*p的方式取修改变量的值了。但是可以通过*p的方式读取变量的值。

#include<stdio.h>

int main()
{
	int a = 10; //定义一个变量

	// 定义一个指向int类型的地址的指针变量,指向a变量的内存地址
	const int *p = &a;   // 指针类型是 const int *
	*p = 100; // 编译会通过不了,常量指针的常量不能改变, 但是直接给a变量赋值还是可以的.只是不允许通过*p的方式
	printf("a=%d\n",*p);
	return 0;
}

2、指针常量
指针常量的const书写在指针类型之后,例如:
char * const p;
定义一个指针常量,一旦初始化之后其内容不可改变,也就是说不能再指向其他变量的地址了。但是可以通过*p的方式改变变量的值。

#include<stdio.h>

int main()
{
	int a = 10; //定义一个变量

	//定义一个指针常量,指向a变量的内存地址
	int *const p = &a; //指针类型是 int *const

	int b = 20; // 允许修改变量
	p = (int *)&b;//编译通不过,因为p是一个常量,不能被修改,指针常量的指针不能修改

	printf("%d\n",*p);

	return 0;
}

原则上在C语言中常量指针的常量不能修改、指针常量的指针不能修改;但是C语言中通过const定义的常量是有问题的,因为可以通过指针变量来间接的修改常量的值,所以在C语言中用#define常量比较多,而C++的const是没办法改的。

1.7 指针与数组的关系

一个变量有地址,一个数组包含若干个元素,每个元素在内存中都有地址,而且是连续的内存地址.也就是说如果数组是char类型的,那么每个元素的内存地址就是相差1, 如果是int类型的数组, 那么元素间的内存地址是相差4,例如:

int a[10];
int *p = a;

p和&a[0]的地址,以及a数组名的地址都是一样的。 也就是说数组名的地址等于数组首元素的内存地址。

#include<stdio.h>

int main()
{
	char a[10];
	printf("a=%p\na[0]=%p\na[1]=%p\na[2]=%p\n",a,&a[0],&a[1],&a[2]);

	printf("========================\n");

	int b[10];
	printf("b=%p\nb[0]=%p\nb[1]=%p\nb[2]=%p\n",b,&b[0],&b[1],&b[2]);
	return 0;
}

输出结果如下:

a=0x16efa760e
a[0]=0x16efa760e
a[1]=0x16efa760f
a[2]=0x16efa7610
========================
b=0x16efa75e4
b[0]=0x16efa75e4
b[1]=0x16efa75e8
b[2]=0x16efa75ec

从结果也可以看出char数组的每个元素内存地址相差1, int数组的元素内存地址相差4.

注意:当指针变量指向一个数组名的时候,C语言语法规定指针变量名可以当做数组名使用.但是指针变量的sizeof不是数组的sizeof, 指针变量的sizeof在64位系统是8个字节, 32位系统是4个字节.

#include<stdio.h>

int main() {
    int a[] = {1, 2, 3, 4, 5};
    int *p; // 指针变量
    // 数组可以直接赋值给指针变量,不用&取地址,因为变量名a的地址就是数组首元素的内存地址
    p = a;

    // 如果指针指向的是数组,那么变量名可以当做数组用
    p[3] = 100;

    // 数组的长度和指针变量的长度是不一样的
    printf("数组长度=%lu字节,指针变量长度=%lu字节\n", sizeof(a), sizeof(p));

    // 操作指针变量来操作数组元素
    int i;
    for (i = 0; i < 5; i++) {
        printf("a[%d]=%d\n", i, p[i]);// 和操作数组一样操作元素
    }
    return 0;
}

输出结果如下:

数组长度=20字节,指针变量长度=8字节
a[0]=1
a[1]=2
a[2]=3
a[3]=100
a[4]=5

注意:指针变量如果指向的不是数组名或者数组的首元素的内存地址,那么用指针去取数组的元素的时候是有区别的,假设指针指向的是数组下标为2的元素的内存地址, 那么p[0] 就不再是等于a[0]了, 而是等于a[2]了, 同理p[1]就应该等于a[3],其他依次类推。

1.8 指针位移运算

指针运算不是简单的整数加减法,而是指针指向的数据类型在内存中占用字节数做为倍数的运算。
例如:char *p;当执行p++;后,移动的字节数是sizeof(char)这么多的字节数;如果是int *p1;那么p1++后移动的字节数就是sizeof(int)

#include<stdio.h>
int main()
{
    int a[5] = {0};
    int *p = a;//指向数组名的指针,就是数组首元素的地址
    *p = 100;// 给指针变量对应地址的变量赋值100,相当于p[0] = 100;

    printf("移动前指针位置:%p\n",p);

    //移动指针
    //移动2,表示移动了sizeof(int) *2 个字节,对应就是数组下标=2的元素内存地址
    p += 2;
    *p = 20;//修改a[2]的值为20
    printf("移动后指针位置:%p\n",p);

    int i;
    for(i = 0 ;i < 5; i++)
    {
        printf("a[%d]=%d\n",i,a[i]);
    }
    return 0;
}

结果如下:

移动前指针位置:0x16efbb600
移动后指针位置:0x16efbb608
a[0]=100
a[1]=0
a[2]=20
a[3]=0
a[4]=0

可以看到指针+2后,移动前后的内存地址刚好相差8个Byte,也就是2个int元素的占用的内存大小.

练习-通过指针位移实现strstr截取子串的功能
思路就是先从源字符串中找到一个开始截取的位置,这个位置通过对比子串的首字符来判断,找到开始位置后就可以逐个字符和子串进行对比, 如果都一样那么就说明找到了,否则会进行下一次循环重新找截取的开始位置.

#include <stdio.h>

char *my_strstr(const char *str, const char *substr)
{
    const char *my_str = str;
    const char *my_substr = substr;
    char *res = NULL; // 最终返回的结果
    while (*my_str)
    {
        if (*my_str != *my_substr)
        {   
            // 逐个取my_str中的字符,直到找到和my_substr的首字符相同时为止
            ++my_str;
            continue;
        }
        // 记录匹配开始的首地址,也就是最终返回的结果
        res = (char *)my_str;
        // 每次都需要参与比较的子串
        char *temp_substr = (char *)my_substr;
        while (*temp_substr)
        {
            if (*my_str != *temp_substr) //后续比较只要不同就跳出
            {
                break;
            }
            // 同时对my_str和temp_substr指针++,实现逐个字符对比
            ++my_str;
            ++temp_substr;
        }
        if (*temp_substr == '\0')
        {
            break; //子串都匹配到末尾了,所以找到了
        }
    }
    return res;
}

int main()
{
    char *str = "hello world";
    printf("res=%s\n", my_strstr(str, "or"));// res=orld
    printf("res=%s\n", my_strstr(str, "ll"));// res=llo world
    printf("res=%s\n", my_strstr(str, "ld"));// res=ld
    return 0;
}

1.9 指针与char数组

在C语言中所有的数据类型都可以当做是一个char的数组.

#include<stdio.h>

int main()
{
	int a = 0x12345678; //定义一个16进制数,int占4个字节,相当于char数组长度是4个BYTE

	char *p = (char *)&a;//定义char * 的指针,指向a的地址

	int i = 0 ;
	for(i =0;i < sizeof(a); i++)
	{
		printf("a[%d]=%x\n",i,p[i]);//由于是小端对齐,所以输出结果是78,56,34,12倒着输出的.内存地址从左->右表示从高->底
	}


	return 0;
}

练习-将一个int数转成ip地址
我们知道一个int占4个字节,刚好对应4个char, 对于unsigned char的取值范围就是0~255, 所以刚好和ip地址每段的取值符合.所以可以将int数转成ip地址。

#include<stdio.h>

int main()
{
	int a = 987654321;

	unsigned char *p = (unsigned char *)&a; //定义unsigned char *类型的指针变量p 对应a变量的地址,这里其实是指针强转
	
	//在C语言中基本数据类型可以当做char数组对待,所以可以这样操作
	printf("%u.%u.%u.%u\n",p[3],p[2],p[1],p[0]); // 结果为: 58.222.104.177

	return 0;
}

直接ping 987654321 对应的ip地址就是上面用程序求出的ip: 58.222.104.177
在这里插入图片描述

练习-将一个字符串IP转成unsigned int类型.
这个案例就是上面案例的反过程, 先将字符串中每一段的数据取出来,然后操作指针来赋值.

#include<stdio.h>

int main()
{
	char a[] = "192.168.1.1";
	unsigned int ip = 0;
	unsigned char * p = (unsigned char *) &ip;

	//先从字符串中取值
	int a1,a2,a3,a4;
	sscanf(a,"%d.%d.%d.%d",&a1,&a2,&a3,&a4);

	//小端对齐赋值
	p[0] = a4;
	p[1] = a3;
	p[2] = a2;
	p[3] = a1;

	printf("ip=%u\n",ip); // ip=3232235777
	return 0;
}

如下所示,对数字3232235777进行ping时,查看的ip刚好就是192.168.1.1
在这里插入图片描述

练习-使用指针对二维数组进行排序

#include<stdio.h>

int main()
{
	int a[2][3] = {{10,21,13},{42,5,9}}; //二维数组

	int *p = (int *)a; // 定义一个指针, 指向数组a
	int i,j;

    //下面通过操作指针的方式来排序, 由于内存是连续的,所以可以当做一维数组看待
	int size = sizeof(a) / sizeof(a[0][0]); // 6
	for(i = 0 ; i < size; i++)
	{
		for(j=0 ; j< size - i -1 ; j++)
		{
			if(p[j] > p[j+1])
			{
       			// 直接通过指针操作内存地址对应的值
				int temp = p[j];
				p[j] = p[j+1];
				p[j+1] = temp;
			}
		}
	}
	//输出排序后的结果
	for( i =0 ; i <sizeof(a) /sizeof(a[0]) ; i++)
	{
		for( j =0; j<sizeof(a[0]) / sizeof(a[0][0]); j++)
		{
			printf("%d,",a[i][j]); // 5,9,10,13,21,42,
		}
	}
	
	return 0;
}

练习-使用指针对字符串取反操作

int main() {
    char str[20] = "hello world";

    char *start = str;//&(str[0]);  //创建首指针
    char *end = start + strlen(str) - 1; //创建尾指针, 通过指针偏移到尾部

    while (start < end) {
        // 通过指针操作值
        char tmp = *start;
        *start = *end;
        *end = tmp;
        // 偏移首位指针
        start++;
        end--;
    }
    printf("%s\n", str); // dlrow olleh

    return 0;
}

1.10 指针数组

由于指针是一个变量,所以也可以以数组的形式出现。 指针数组的字节长度不受数据类型的影响,而是受到操作系统的影响, 在32位系统中,一个指针的长度是4个BYTE, 64位系统是8个BYTE, 所以32位系统中指针数组的长度等于 元素个数*4 ; 64位系统中指针数组的长度等于元素个数*8.

#include<stdio.h>

int main()
{
	//定义一个int *类型的10个元素的指针数组
	int *a[10] = {NULL};

	//定义一个char *类型的10个元素的指针数组
	char *b[10] = {NULL};

	printf("%lu,%lu\n",sizeof(a),sizeof(b)); // 80,80

	return 0;
}

指针数组元素的赋值

#include<stdio.h>

int main()
{
	int *a[10] = {NULL};
	
	int i = 5;
	a[0] = &i; //取i的地址给a[0]指针

	*a[0] = 100; //修改i的值

	printf("i=%d\n",i); // i=100
	
	return 0;
}

1.11 指针的间接赋值和取值

任何一块连续的内存空间,都可以操作指针来间接赋值和取值,有2种方式

  • 通过指针偏移,然后取*操作后当做左值使用就可以被赋值了,当做右值就可以取值了.
  • 通过指针变量名[index]的方式,当做左值使用就可以被赋值了,当做右值就可以取值了.
#include <stdio.h>

int main()
{
    int a[] = {1, 2, 3, 4};
    int *p = a; //指针默认指向首元素的地址
    // 两种取值方式
    int i = *(p + 1);
    int j = p[2];
    printf("取值:a[1]=%d,a[2]=%d\n", i, j); // 取值:a[1]=2,a[2]=3

    // 两种赋值方式
    *(p + 2) = 100;
    p[3] = 200;
    printf("赋值:a[2]=%d,a[3]=%d\n", a[2], a[3]); // 赋值:a[2]=100,a[3]=200

    return 0;
}

1.12 一维数组指针

当指针指向数组的地址时,此时指针的步长就是sizeof(arr)了,也就是数组所有元素的大小之和.
下面介绍3种方式来定义数组的指针:

  • 先定义数组类型,然后再定义数组指针类型
#include <stdio.h>

// 1.自定义一个数组类型,注意圆括号内的是类型的名称,这个和非数组类型的定义方式不一样
// 非数组类型的定义方式形如: typedef unsigned int u32; 表示定义了一个无符号的int类型
typedef int(ARRY5)[5]; 


int main()
{
    // 2.接着就可以定义它的指针类型了
    ARRY5 arr = {1, 2, 3, 4, 5}; //等效于int arr[5] = {1,2,3,4,5};
    ARRY5 *p = &arr;
    printf("%lu\n", sizeof(arr)); // 20
    printf("%d\n", p);            // 1082189744
    printf("%d\n", (p + 1));      // 1082189764,和上面的数相差20,也就是刚好是sizeof(arr)

    for (int i = 0; i < 5; ++i)
    {

        // printf("arr[%d]=%d\n",i,p[0][i]);
        // printf("arr[%d]=%d\n",i, (*p)[i]);
        printf("arr[%d]=%d\n", i, *(*p + i));
        //上面*p和p[0]效果一样都是为了取出指针的值也就是arr数组,数组名也是指针,所以*(*p)=数组第一个元素
    }
    return 0;
}
  • 先定义数组指针的类型然后指向数组的地址
#include <stdio.h>

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    typedef int(*ARRAY5)[5]; // 直接定义数组指针类型
    ARRAY5 p = &arr;         // 然后使用指针指向数组地址
    for (int i = 0; i < 5; i++)
    {
        printf("arr[%d]=%d\n", i, *(*p + i)); //*p得到数组名,数组名也是指针类型,*p+i移动指针,然后*(..)是取值
    }
    return 0;
}

  • 直接定义数组指针变量并指向数组地址

注意是变量,而不是类型哦

#include <stdio.h>

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    int(*pArray)[5] = &arr; // 这里pArray是一个指针变量名,而不是类型哦
    for (int i = 0; i < 5; i++)
    {
        printf("arr[%d]=%d\n", i, *(*pArray + i));
    }
    return 0;
}

1.13 二维数组指针

二维数组和一维数组一样,除了使用sizeof和使用&取数组地址的这2种情况外都可以把数组名当做指针使用,是指向首元素地址的指针,只不过二维数组的首元素是一维数组, 所以该指针是指向一维数组的指针,指针的步长是一个一维数组的大小.

#include <stdio.h>

int main()
{
    int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    printf("%d\n",sizeof arr[0]); //12,每个一维数组的大小都是12,因为一维数组有3个int元素
    printf("%d\n",arr); //1710757312
    printf("%d\n",arr+1);//1710757324,和上面相差12, 也就是一个一维数组的大小
}

既然二维数组的数组名是一个指向一维数组的指针变量,那么这个二维数组名其实就可以看成int(*pArray)[3]的指针变量了.

#include <stdio.h>

int main()
{
    int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int(*pArray)[3] = arr; //由此可见arr的类型是一个指向一维数组的指针类型.
    for (int i = 0; i < 3; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            // printf("%d\n",pArray[i][j]);//pArray[i]得到每一个一维数组
            printf("%d\n", (*(pArray + i))[j]); //*(pArray+i)也可以得到每一个一维数组,等效于pArray[i]
        }
    }
}

当然我们可以不用这么麻烦,直接把数组名arr当做指针变量来使用,例如通过指针方式获取arr[1][2]的值还可以这么操作

//通过指针方式获取arr[1][2]的值
//1.arr是一维数组指针类型,arr+1表示指针偏移一个步长,也就是指向arr[1]的地址
//2.*(arr+1)得到arr[1]的一维数组,一维数组名也是指项它首元素的指针,类型是int*
//3.*(arr+1)+2表示int*类型的指针偏移2个步长,此时指向的就是arr[1][2]元素的地址
//4.*(*(arr+1)+2)表示取arr[1][2]地址的值
int num =  *(*(arr+1)+2); 
printf("%d\n",num); //6

因此,当二维数组传递给函数参数的时候可以这样写

#include <stdio.h>

void printArray(int (*arr)[3], int len1, int len2) //二维数组当做函数参数传递时会退化为指向一维数组地址的指针
{
    for (int i = 0; i < len1; ++i)
    {
        for (int j = 0; j < len2; ++j)
        {
            printf("arr[%d][%d]=%d\n", i, j, *(*(arr + i) + j)); // 等效于arr[i][j];
        }
    }
}

int main()
{
    int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    printArray(arr, 3, 3);
    return 0;
}

二维数组的指针取*操作后得到的是一维数组,而一维数组取*后得到的是数组首元素。

二、二级和多级指针

指针就是一个变量,既然是变量就也就存在内存地址,所以可以定义一个指向指针的指针变量。二级指针的定义会比一级指针多一个*;
二级指针指向的是一级指针的地址, 一级指针指向的是变量的地址, 如下图所示:
在这里插入图片描述
换成代码就是:

int a =  10;
int *p = &a; //一级指针指向变量a
int **pp = &p; //二级指针指向一级指针变量p

//反过来就是修改变量的值
*pp  = NULL; // 表示将1级指针变量的值赋值为NULL;
**pp  = 100; //表示把变量a的值赋值为100; 

2.1 二级指针与一级指针数组的关系

一级指针数组里面存放的元素都是一级指针变量, 而指针数组的名字就是数组首元素的内存地址, 也就是一级指针的内存地址, 所以可以用二级指针的来指向.当指针指向数组的时候就可以当做数组本身来使用了.

int main()
{
	int *a[3] = {NULL};//一级指针数组
	int x,y,z;
	a[0] = &x; //给数组元素赋值
	a[1] = &y;
	a[2] = &z;

	int **p = a;//二级指针指向数组名,数组名等效于指向&a[0],而a[0]是个一级指针.所以指向一级指针地址的指针那就是二级指针了.
		
	//指针指向数组就可以当做数组用

	int i;
	for(i=0;i<3;i++)
	{
		printf("p[%d]=%p\n",i,p[i]); //打印数组元素,数组元素是一级指针
	}
	
	*p[0] = 100;//等效于 *a[0] =100; 就是将0下标的指针指向的变量的值修改为100,因为p[0]=a[0]=一级指针,然后加*就是修改值
	*p[1] = 200;
	*a[2] = 300;
	printf("x=%d,y=%d,z=%d\n",x,y,z);

	
	return 0;
}

结果如下:

p[0]=0x16b1735f8
p[1]=0x16b1735f4
p[2]=0x16b1735f0
x=100,y=200,z=300

2.2 多级指针

每多加一级指针, 指针类型的*号就会多一个.

#include<stdio.h>

int main()
{
	int a = 10;
	int *p = &a; // 一级指针
	int **pp = &p; //二级指针
	int ***ppp = &pp; //三级指针
	int ****pppp = &ppp; //四级指针
	int *****ppppp = &pppp; //五级指针

	*****ppppp = 100;//修改a的值
	printf("a=%d\n",a);
	
	return 0;
}

三、指针在函数中的应用

3.1 在函数参数中使用指针

在C语言中规定,如果想通过函数内部修改外部实参的值,那么就需要给函数的参数传递实参的地址,否则是无法修改的。因为函数内对形参的改变不会影响到外部实参的值。

#include<stdio.h>

//错误的交换姿势
void swap1(int a, int b )
{
	int temp = a;
	a = b;
	b = temp;
}


//正确的交换姿势,形参定义为指针, 可以接收变量的地址
void swap2(int *a,int *b)
{
	//下面的交换就是交换地址对应的变量的值,地址是不会改变的,这样可以保证唯一性
	int temp = *a; //取a指针变量对应的地址的变量的值
	*a = *b; //取b指针变量对应的地址的变量的值,赋值给a变量
	*b = temp;
}

int main()
{
	int a = 1;
	int b = 2;
	swap1(a,b);
	printf("swap1->a=%d,b=%d\n",a,b);
	
	swap2(&a,&b); //传递变量的地址
	printf("swap2->a=%d,b=%d\n",a,b);

	return 0;
}

结果如下:

swap1->a=1,b=2
swap2->a=2,b=1

可以看到swap1并没有交换成功, 而swap2成功了.

3.2 数组名作为函数的参数

C语言规定当一个数组名作为函数的形参的时候,它就变成了指针变量了,而不再是数组名了。
指针是什么类型就看数组首元素是什么类型,例如数组首元素是int类型,那么指针类型就是int*, 如果数组是double类型就是double*,如果是int*类型,那么指针就是int**类型.
所以下面3种参数的写法其实是一样的

void test1(int a[10]);
void test2(int a[]);
void test3(int  *a);  //第三种写法常用

//第三种写法如果不想被函数的内部修改元素的值, 那就可以这样定义
void test3(const  int *a);  //相当于指向常量的指针 ,当然别人想改还是可以强转来修改的

注意:如果把一个数组名作为函数的参数,那么在函数内部就不知道这个数组的元素个数了,因为sizeof算出来的是指针的大小, 32位系统是4个BYTE, 64位是8个BYTE, 所以如果想知道数组的长度,就需要增加一个参数来标明这个数组的大小. 除非数组是一个字符串, 那么可以不用传递长度.因为字符串可以有strlen方法来获取长度

#include<stdio.h>

//void test(int a[10])
//void test(int a[]);
void test(int *a) //如果传入的是数组名,就变成了指针变量,所以可以这样写
{
    printf("test->sizeof=%lu\n",sizeof(a)); // test->sizeof=8

    //操作指针就可以操作数组
    a[2]  = 100;
}

int main()
{
    int a[10] = {0,1,2,3,4,5,6,7,8,9};
    test(a); //将数组名传入
    printf("main->sizeof=%lu\n",sizeof(a)); // main->sizeof=40 

    int i;
    for(i=0;i<sizeof(a)/sizeof(a[0]);i++)
    {
        printf("a[%d]=%d,",i,a[i]);//a[0]=0,a[1]=1,a[2]=100,a[3]=3,a[4]=4,a[5]=5,a[6]=6,a[7]=7,a[8]=8,a[9]=9,
    }

    return 0;
}

当数组传递给函数参数后,由于会变成了指针变量,因此无法在函数内部获取数组的长度,所以需要额外传递数组的长度作为参数给函数内部使用

#include <stdio.h>

void printIntArray(int *arr, int len) // 如果传递的是数组名就会退化为指针
{
    for (int i = 0; i < len; i++)
    {
        // printf("arr[%d]=%d\n",i,arr[i]);
        printf("arr[%d]=%d\n", i, *(arr + i));
    }
}

void printStringArray(char **arr, int len) // 如果传递的是数组名就会退化为指针
{
    for (int i = 0; i < len; i++)
    {
        //printf("arr[%d]=%s\n", i, arr[i]);
        printf("arr[%d]=%s\n", i, *(arr + i));
    }
}

int main()
{
    int a[] = {1, 2, 3, 4, 5};
    printIntArray(a, 4);

    char *str[] = {"aaaa", "bbbb", "cccc", "dddd"};
    printStringArray(str, 4);

    return 0;
}

3.3 函数指针

所谓函数指针就是指向函数内存地址的指针。一个函数在编译的时候会分配一个入口地址,这个入口地址就是函数的指针,函数名就是函数的入口地址。

函数指针的定义方式:
函数的返回值类型(*指针变量名称)(参数列表)
例如:
int(*p)(int) 定义一个指向int func(int n)类型的函数地址的指针.

函数可以通过函数指针来调用.
int(*p)()代表一个指向函数的指针,但是没有固定指向哪一个函数, 这个可以动态指向某一个函数名.

void man() {
    printf("抽烟\n");
    printf("喝酒\n");
    printf("打牌\n");
}

void woman() {
    printf("化妆\n");
    printf("逛街\n");
    printf("网购\n");
}

int main() {
    void (*p)(); //定义一个无返回值,无参数的函数指针,指针变量名是p
    int i = 0;
    scanf("%d", &i); // 读取用户输入
    if (i == 0)
        p = man; //指向man函数
    else
        p = woman; //指向woman函数
    p(); //通过指针调用函数
    return 0;
}

函数指针可以直接调用函数,编译器会自动取*,当然也可以手动取*, 例如(*p)();

3.4 回调函数

将函数的指针作为另一个函数的参数时,这个参数就被称为回调函数。

#include <stdio.h>

int max(int a, int b) {
    if (a > b)
        return a;
    else
        return b;
}

int add(int a, int b) {
    return a + b;
}

//参数1是一个函数指针(回调函数)
void func(int(*p)(int, int), int a, int b) {
    int res = p(a, b);//调用p指针对应的函数执行
    printf("%d\n", res);

}

int main() {
    int i = 0;
    scanf("%d", &i); // 接收控制台输入的值
    if (i == 0)
        func(max, 10, 20); //传参的时候,直接传入函数的名字,因为函数名就是地址,可以用函数指针接收
    else
        func(add, 10, 20);
    return 0;
}

练习-定义打印任意类型的数组

#include <stdio.h>
//定义一个打印任意类型数组的函数
//参数1:数组名
//参数2:元素的长度
//参数3:数组的长度
//参数4:回调函数
void print_array(void *arr, int element_size, int arr_size, void (*print_fun)(void *))
{
    //先修改指针的步长为1个字节
    char *my_arr = (char *)arr;
    //根据element_size,偏移指针到数组的指定元素的首地址
    for (int i = 0; i < arr_size; ++i)
    {
        //每次指针偏移的长度是 element_size * i
        char *element = my_arr + element_size * i;
        //通过回调函数实现具体的数据打印,因为只有调用方知道元素的具体类型
        print_fun(element);
    }
}

//打印int类型的数组的回调函数
void print_int_array(void *data)
{
    //修改指针步长为int类型
    int *element = (int *)data;
    printf("%d\n", *element);
}

struct Person
{
    char name[30];
    int age;
};
//打印结构体的回调函数
void print_person_array(void *data)
{
    //修改步长为struct Person类型
    struct Person *p = (struct Person *)data;
    //打印内容
    printf("name=%s,age=%d\n", p->name, p->age);
}

int main()
{
    //定义int数组
    int arr[] = {1, 2, 3, 4, 5};
    print_array(arr, sizeof(int), 5, print_int_array);

    //定义结构体数组
    struct Person person_array[3] = {{"tom", 20}, {"jack", 30}, {"petter", 40}};
    print_array(person_array, sizeof(struct Person), 3, print_person_array);
    return 0;
}

3.5 函数指针数组

既然函数指针也是变量,那么就可以存在数组的形式,定义的格式是:
返回值(*函数指针变量名[num])(参数列表)

#include <stdio.h>
void fun1()
{
    printf("func1 run...\n");
}
void fun2()
{
    printf("func2 run...\n");
}
void fun3()
{
    printf("func3 run...\n");
}

int main()
{
    void (*func_array[3])(); // 定义函数指针数组
    // 存入3个函数
    func_array[0] = fun1;
    func_array[1] = fun2;
    func_array[2] = fun3;

    for (int i = 0; i < 3; ++i)
    {
        func_array[i](); // 从数组取出每一个函数指针,然后执行
        //或者对函数指针取*执行
        //(* func_array[i])();
    }
    return 0;
}

3.6 指针数组作为mian函数的形参使用

int main(int argc, char ** argv)  //参数2是二级指针,可以接收一级指针数组, 写法等效于char *argv[]

main函数是操作系统调用的,所以main函数的参数也是操作系统在调用的时候自动填写的,其中argc打包命令行参数的数量, argv代表命令行的具体参数.

下面写一个计算 2个数的和的程序,通过main函数的参数来完成.

#include<stdio.h>
#include<stdlib.h>

int main(int argc,char **args)
{
	if(argc <=2)
	{
		printf("参数不足,使用方式:%s 数字1 数字2\n",args[0]);
		return 0;
	}

	int a = atoi(args[1]); //char *类型可以直接访问字符串
	int b = atoi(args[2]);
	printf("sum=%d\n",a+b);

	return 0;
}

下面是一个四则运行实例代码

#include<stdio.h>
#include<stdlib.h>

int main(int argc,char **args)
{
	if(argc <=3)
	{
		printf("参数不足,使用方式:%s 数字1  运算符  数字2\n",args[0]);
		return 0;
	}

	int a = atoi(args[1]); //char *类型可以直接访问字符串
	char *c  = args[2];   //接收运算符, 注意char*指向的是字符串而不是char , 是双引号的字符串内容
	int b = atoi(args[3]);
	
	switch(c[0])  //取字符串首字符
	{
		  case '+':
			printf("%d%s%d=%d\n",a,c,b,a+b);
			break;
       	    case '-':
                    printf("%d%s%d=%d\n",a,c,b,a-b);
                    break;
    	    case '*':   //注意乘法在控制台输入的时候需要转义 \* ,否则在Linux上代表是所有的意思
                    printf("%d%s%d=%d\n",a,c,b,a*b);
                    break;
    	    case '/':
                    printf("%d%s%d=%d\n",a,c,b,a/b);
                    break;
	}
	return 0;
}

3.7 指针作为函数参数的输入和输出特性

3.7.1 输入特性

主调函数分配内存(栈/堆),被调函数使用

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void printIntArray(int *arr, int len) //如果传递的是数组名就会退化为指针
{
    for (int i = 0; i < len; i++)
    {
        // printf("arr[%d]=%d\n",i,arr[i]);
        printf("arr[%d]=%d\n", i, *(arr + i));
    }
}

void printStringArray(char **arr, int len) //如果传递的是数组名就会退化为指针
{
    for (int i = 0; i < len; i++)
    {
        //printf("arr[%d]=%s\n", i, arr[i]);
        printf("arr[%d]=%s\n", i, *(arr + i));
    }
}

void printString(char *s)
{
    printf("%s", s);
}

int main()
{
    int a[] = {1, 2, 3, 4, 5}; //主调函数分配内存(栈)
    printIntArray(a, 4);       //被调函数使用

    char *str[] = {"aaaa", "bbbb", "cccc", "dddd"}; //主调函数分配内存(栈)
    printStringArray(str, 4);                       //被调函数使用

    char *s = malloc(sizeof(char) * 100); //主调函数分配内存(堆)
    memset(s, 0, 100);
    strcpy(s, "helloworld");
    printString(s); //被调函数使用
    free(s);
    s = NULL;
    return 0;
}

输出结果:

arr[0]=1
arr[1]=2
arr[2]=3
arr[3]=4
arr[0]=aaaa
arr[1]=bbbb
arr[2]=cccc
arr[3]=dddd
helloworld

3.7.2 输出特性

被调函数分配内存(栈/堆),主调函数使用.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void init(char **str)
{
    // 被调函数分配内存后才能使用
    char *temp = malloc(sizeof(char) * 100);
    memset(temp, 0, 100);
    strcpy(temp, "helloworld");
    *str = temp;
}

int main()
{
    char *p = NULL;
    init(&p); // 主调函数使用,需要取地址
    if (p)
    {
        printf("p=%s\n", p); // p=helloworld 
        free(p);
        p = NULL;
    }
    return 0;
}

总结:
1.输入特性中不需要传递指针变量的地址,因为不涉及更改指针的值,而输出特性中需要传递指针变量的地址,需要更改指针变量的值.
2.具体是输入还是输出特性是相对于被调函数的, 被调函数能够直接使用,那就是输入,不能直接使用就是输出.

四、内存操作与指针

memset、memcpy、memmove这3个函数分别是内存的设置、内存的拷贝和内存的移动。使用时需要引入string.h头文件。

4.1 memset

表示设置内存,接收3个参数

void *memset(void *s, int c,  size_t n);

参数1:是一个无类型的指针, 用于接收一个内存首地址,如果要操作数组,那就传数组名,因为数组名对应的是首元素的内存地址. 可以被指针变量接收.
参数2:要设置的值,如果填0就是0.
参数3:表示这块内存的大小,单位是字节
例如:

#include<stdio.h>
#include<string.h>

int main()
{
	int a[5] = {1,2,3,4,5};
	//清空a数组的值
	memset(a,0,sizeof(a));

	int i;
	for(i=0;i<5;i++)
	{
		printf("a[%d]=%d,",i,a[i]); // a[0]=0,a[1]=0,a[2]=0,a[3]=0,a[4]=0,

	}

	return 0;
}

4.2 memcpy

用于在2块内存之间拷贝数据,接收3个参数.

void *memcpy(void *dest,  const void *src,   size_t  n);

参数1:目标地址的指针变量
参数2:源地址的指针变量, 且不能修改源地址变量的值,他是const修饰的.
参数3:拷贝多少内容,单位字节

#include<stdio.h>
#include<string.h>

int main()
{
	int a[5] = {1,2,3,4,5};

	//拷贝到b数组
	int b[10] = {0};
	memcpy(b,a,sizeof(a)); //参数1和2都是无类型的,所以其他数据类型,包括字符串都可以使用.
	int i;
	for(i=0;i<10;i++)
	{
		printf("b[%d]=%d,",i,b[i]);//b[0]=1,b[1]=2,b[2]=3,b[3]=4,b[4]=5,b[5]=0,b[6]=0,b[7]=0,b[8]=0,b[9]=0,

	}

	return 0;
}

再来看看字符串的拷贝

#include<stdio.h>
#include<string.h>

int main()
{
	char a[] = "hello"; //定义一个字符串
	char b[10] = {0}; //定义一个char数组,char数组元素为0时,用%c输出的时候是看到不到的.
      //拷贝
	memcpy(b,a,sizeof(a));

	int i;
	for(i=0;i<10;i++)
	{
           //输出b的每个元素的值
		printf("b[%d]=%c\n",i,b[i]);
	}
  //拷贝完后, b就变成字符串了
	printf("b=(%s)\n",b);
	return 0;

}

使用memcpy的时候,一定要确保内存没有重叠区域.
例如 :

int a[10] ={0}; 
memcpy(&a[3], &a[0], 20);  //拷贝20个字节也就是5个int数据

将a[0]之后的5个int数据拷贝到a[3]之后的5个int数据,他们中间下标3和4之间存在内存重叠区域.

4.3 memmove

表示内存移动, 参数也memcpy一样,用法也一样.

#include<stdio.h>
#include<string.h>

int main()
{
	int a[10] = {0,1,2,3,4,5,6,7,8,9};
	int b[10] = {0};
    //移动a的内存到b,并不会改变a的内存
	memmove(b,a,sizeof(a));

	int i;
	for(i=0;i<10;i++)
	{
		printf("a[%d]=%d,b[%d]=%d\n",i,a[i],i,b[i]);
	}

	return 0;
}

结果如下:

a[0]=0,b[0]=0
a[1]=1,b[1]=1
a[2]=2,b[2]=2
a[3]=3,b[3]=3
a[4]=4,b[4]=4
a[5]=5,b[5]=5
a[6]=6,b[6]=6
a[7]=7,b[7]=7
a[8]=8,b[8]=8
a[9]=9,b[9]=9

五、指针和字符串

在C语言中,大多数的字符串操作其实就是指针的操作.

5.1 通过指针修改字符串中的字符

#include<stdio.h>

int main()
{
	char s[] = "hello world";
	char *p = s; //数组名就是首元素的地址, 也就是'h'字符的地址, 所以*p取的值是'h'

	printf("*p=%c\n",*p); //取首元素的值,*p=h
	
    p[0] = 'a'; //修改首元素的值,相当于s[0]='a'

	printf("*p=%c\n",*p);// *p=a
	return 0;
}

5.2 通过指针偏来修改字符串

#include<stdio.h>

int main()
{
    char buf[100] = "hello world";
    char *p = buf; // 指向字符串首地址

    printf("*p=%c\n",*p); // *p=h

    *(p+3) = 'A';//修改当前指针后3个地址对应的值,注意此时指针没有移动, 指向的还是首地址,它的值还是'h'

    printf("*p=%c,buf=%s\n",*p,buf); // *p=h,buf=helAo world


    //移动指针
    p+=6; //+=赋值,会真正移动指针
    printf("*p=%c\n",*p); // *p=w

    *p = 'B';//修改此时指针指向地址的值
    printf("buf=%s\n",buf); // buf=helAo Borld

    return 0;
}

5.3 char *和字符串的关系

由于char * 是一级指针可以接收一个字符串变量(char 数组名) 或者字符串常量,所以按照%s输出指针变量其实是和输出数组名的效果一样的, 都是字符串内容. 如下所示:

char a[] = "hello"; //定义一个字符串
char *c = a; //一级指针指向数组名,可以当做数组使用
printf("c=%s,a=%s\n",c,a);  //结果都是hello字符串

字符串常量和字符串变量定义和区别
1、字符串常量
定义:在一个双引号""内的字符序列或者转义字符序列称为字符串常量
例如:“HA HA!” “abc” “\n\t”
这些字符串常量是不能改变的,如果试图改变指针所指向的内容是错误的,因为字符串常量是存在静态内存区的,不可以改变的。
如定义字符串常量:

char* a="i love you.";
*a='h';  //试图改变它

这是错误的。系统显示:

windows上报错如下:
string.exe 中的 0x00d71398 处未处理的异常: 0xC0000005: 写入位置 0x00d7573c 时发生访问冲突或者报该内存不能为written。

在gcc环境下报错如下:
Process finished with exit code 138 (interrupted by signal 10: SIGBUS)

2、字符串变量
在C语言中没有纯粹的c语言字符串变量,可以通过一个字符数组来体现,这样就可以对字符数组中的内容进行改变!
如定义字符串变量:

char a[]="i love you.";
*a='h'; //将数组首地址对应的元素的值修该为h

5.4 char ** 和char*[]以及字符串的关系

char ** 是二级指针,用于接收一级指针的地址, 而char *[]是一级指针数组, 数组名就是首元素(一级指针)的地址,所以可以被char ** 接收, 那如果char ** 指向数组名,那么就可以当做数组使用. 所以就有下面的关系:

char a[] ="hello";//定义一个字符串变量,字符串是特殊的字符数组
char *s[2]; //定义一个一级指针数组,元素都是一级指针
s[0]  = a;//一级指针指向字符串数组名,数组名就是地址,可以当做a来使用, 也就是char *可以当做a使用
char **p = s; //二级指针指向一级指针数组名, 可以当做s来使用

printf("p[0]=%p,s[0]=%p,a=%p,a[0]=%p\n",p[0],s[0],a,&a[0]); //结果都是同一个地址, p[0]的类型是char *,所以*p取出的也是char *类型
printf("p[0]=%s,s[0]=%s,a=%s,a[0]=%c\n",p[0],s[0],a,a[0]);//结果都是字符串内容hello, 除了a[0] = 'h'

printf("*p=%p,**p=%c\n",*p, **p);//*p的结果类型是char * ,而**p的结果类型是char

结果如下:

p[0]=0x16b4bb5fc,s[0]=0x16b4bb5fc,a=0x16b4bb5fc,a[0]=0x16b4bb5fc
p[0]=hello,s[0]=hello,a=hello,a[0]=h
*p=0x16b4bb5fc,**p=h

总结: char** 指向char*的数组, 所以char**的指针变量可以当做char*数组用, 而char* 指向char数组,所以char *可以当做char数组用, 因此char **的指针变量可以直接操作char数组的元素.

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值