【C语言】指针与数组

字符指针与字符数组

先来看看下面这个表达式

char* p = "abcdef";

p存放的地址实际上是字符串中首字符的地址,而不是整个字符串的指针。
若打印

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

结果显示为 a
这就证明了这个理论。

上述例子的"abcdef"是一个常量字符串,不能进行任何改变
若进行以下运算:

*p = 'b';
printf("%c\n",*p);
printf("%s\n",p);

则编译器则会报错,程序会崩溃,这是因为常量字符串放在内存中的常量区,是不能被修改的,为了保护常量字符串不被修改,则需要使用const 修饰符对其进行保护防止被写入
声明定义时应该写成:

const char* p = "abcdef";

赋值给字符指针和字符数组的区别
来看看以下例子

const char* p1 = "abcdef";
const char* p2 = "abcdef";

char arr1[] = "abcdef";
char arr2[] = "abcdef";

if(p1==p2)
{
    printf("p1==p2\n");
}
else
{
    printf("p1!=p2\n");
}

if(arr1==arr2)
{
    printf("arr1==arr2\n");
}
else
{
    printf("arr1!=arr2\n");
}

结果打印:
p1==p2
arr1!=arr2

例子中的常量字符串由于不可被修改,为了节省空间,编译器一般只会为一个常量字符串开辟一个空间,所以p1和p2都指向同一个空间。
而使用常量字符串给字符数组赋值的时候,编译器会为每一个数组在栈区单独开辟空间,然后再使用常量字符串给字符数组的元素赋值,由于每一个数组都有自己单独的空间,所以首元素地址不相等。

指针数组和数组指针

从字面上理解,
指针数组本质上是一个数组,而数组中存放的元素为指针;
而数组指针本质上是一个指针,指向一个数组。

指针数组的声明:
以下的声明:

int* arr[10];

表示的是arr为一个数组,数组中存在10个元素,每个元素都为指针,而这些指针每个都指向一个整型变量;

int arr1[5] = {1,2,3,4,5};
int arr2[5] = {2,3,4,5,6};
int arr3[5] = {3,4,5,6,7};

int* parr[3] = {arr1, arr2, arr3};//将每个数组首元素地址放入parr中

for(int i = 0;i<3;i++)//遍历所有元素
{
    for(int j = 0;j<5;j++)
    {
        printf("%d ",*(parr[i]+j));
    }
    printf("\n");
}

以上例子中的指针数组parr构造了一个类似二维数组的结构,但是和二维数组不一样的是,二维数组的所有元素都是在内存空间中连续存放的,但是parr构造的结构,只有每一行元素是连续的,隔行的元素就不是连续的了。

数组指针的声明:
以下的声明:

int (*p)[10];

表示的是p为一个指针,指针指向一个数组,该数组存在10个元素,每个元素为整型变量。
    
若定义一个数组

int arr[10] = {0};

若要取到数组的地址,并保存至p中。则

p = &arr;


arr和&arr有什么不同呢?
若打印变量arr和&arr,得到的地址是一模一样的,
在编译器中打印为

0333F7B4
0333F7B4

但是若打印arr+1 和 &arr+1,则分别打印arr数组的第二个元素的地址和arr数组的地址。
在编译器中打印为

0333F7B8
0333F7DC

由此可见&数组名和数组名的值是一样的,但是代表的含义完全不一样。&数组名为数组的地址,而数组名则是数组首元素的地址。
再来看看下一个例子,若要定义一个打印arr[3][5]二维数组的函数
一般我们会这样声明

void printArr(int arr[3][5], int row, int col);

        但是一般数组传参会弱化为指针,那传入函数的指针是什么类型的指针呢,一般我们传的数组指针都是会在函数中弱化为首元素的地址,所以传入的指针的值应该为二维数组的首元素地址。
        那二维数组的首元素是什么呢?
        arr可以看做一个有3个元素的一维数组,而每个元素都是一个有5个元素的一维数组。
所以传入的指针应该为一个数组指针,这个数组指针则为指向一个有5个整型变量的数组,则可以这样定义

void printArr(int (*p)[5]), int row, int col)
{
    for(int i = 0;i<3;i++)//遍历所有元素
    {
        for(int j = 0;j<5;j++)
        {
            //(p+i)指向第i行
            //*(p+i)相当于拿到第i行的数组名,
            //数组名相当于首元素地址,*(p+i)就是第i行的第一个元素的地址
            printf("%d ",*(*(p+i)+j));//或者写为p[i][j],p[i]和*(p+i)的含义是一样的
        }
        printf("\n");
    }
}

这也就解释了一般我们在声明这种入参为二维数组地址的时候,只有行数可以省略而列数不可省略

void printArr(int arr[][5], int row, int col);

指针传参和数组传参

若存在以下数组

int arr1[10];

对于arr1,如果有以下函数的声明:

void test(int arr[]);
void test(int arr[10]);
void test(int* arr);

当传入数组名时

test(arr);

可以直接在函数中声明arr[10],但是一般不建议。
也可以直接忽略数组大小。填写arr[]。
arr数组名是首元素的地址,是一种整型指针类型,因此可以使用int *作为函数的入参声明

若存在以下数组

int* arr[20];

对于arr,同理,有以下函数的声明都可以使用:

void test(int* arr[20]);
void test(int* arr[]);
void test(int** arr);

为什么能够使用第三个二级指针的声明呢?
因为arr中的元素是整型指针,若使用一个变量接收这个首元素的地址的值,则应该为

int **p = &arr[0];

所以可以使用二级指针来做声明类型

若存在以下二维数组

int arr[3][5];

对应arr,若要接受数组作为参数,则有以下函数的声明:

void test(int arr[3][5]);
void test(int arr[][5]);
void test(int (*arr)[5]);

若要传入数组,则行数可以省略,但是列数不能省略。
因为传入的首元素是一个数组指针,必须要指定该数组指针的大小

技巧:
当传入数组名作为函数形参时,该函数的声明可以省略数组名最近的右边的[]中数字,其他地方的[]数字则不能省略,而也可以将[]转换为指针。比如说
void test(int* arr[3][5]);
这个函数声明中的可以省略arr最近的右边的[]中数字,为void test(int* arr[][5]),也可以转换为指针void test(int* (*arr)[5])  (加括号是因为运算符优先级的问题)。

 函数指针

若存在一个函数

int test(int a, int b);

若定义一个函数指针
则应该定义为

int (*pf)(int, int) = test;

此时pf存放了test函数的地址
若要使用pf来调用test函数,则可以写为

int c = (*pf)(1,2);

也可以不使用解引用运算。

int c = pf(1,2);

这里是函数指针和其他指针类型的不一样之处,可以视为解引用* 多余的。
这也就表明函数名可以看做是函数的地址。

复杂声明类型

可以看一下在《C陷阱与缺陷》中的一个例子

(*((void(*)())0)();

首先由里到外,先看一下

void(*)()

这个是一个函数指针的类型,该函数是一个参数为空,返回值为void的函数。
而((void(*)())0,是强制转换的操作,将0的地址处转换为一个函数指针。
(*((void(*)())0)(),所以这个表达式的意思是将0强制转换为一个指针类型,这就意味着0地址处放置了一个参数为空,返回值为void的函数,然后再调用0地址处的函数。

再来看书籍中的另外一个例子

void  (*signal(int , void(*)(int)))(int);

 首先比较清楚signal是一个函数,
而void(*)(int)是一个函数指针,
signal(int , void(*)(int))则是一个入参为int 和一个函数指针的函数,那返回值这就是
一个指针(*signal(int , void(*)(int))),
而这个指针由指向了一个函数,这个函数的入参是int,返回值则是void 类型。

技巧:
在应对这种复杂的类型时,要清楚其本质,可以从变量的附近开始,按照运算符优先级进行解读,一般是从右开始再到左,按照广度优先的方法从近到远进行解读,
若遇到*,则是一个指针,讨论该指针指向何处,
若遇到[],则是一个数组,讨论其数组中的元素是什么
若遇到(),则是一个函数,讨论其入参和返回值是什么

例如以上的第二个用例

void  (*signal(int , void(*)(int)))(int);

首先从signal解读,从右边开始是一个圆括号,则表明signal是一个函数,那函数就必须要有入参和返回值
入参就是两个值,第一个参数是int 类型, 而第二个参数则是void(*)(int)类型(这个类型也可以继续按照规律解读,可以理解为这个类型为一个函数指针,入参为int 类型,返回值是void类型),
signal返回值则是一个指针,而指针指向一个函数,函数中的参数为int 类型,返回值是void 类型

typedef简化复杂声明
一般在实际项目中尽量不要声明复杂类型,但是如果必须得这样声明,可以使用typedef来简化
对于第一个例子中的

(*((void(*)())0)();

可以使用以下类型重定义

typedef (void(*funcptr)();
(*(funcptr)0)();

对于第二个例子中的

void  (*signal(int , void(*)(int)))(int);

可以使用以下类型重定义

typedef void(*HANDEL)(int);
HANDEL signal(int , HANDEL);

函数指针的运用——转移表和回调函数

使用转移表我们首先要定义一个函数指针数组,来存储相同类型的函数指针,比如说以下的函数

int Add(int x, int y)
{
	x+y;
}
int Sub(int x, int y)
{
	x-y;
}
int Mul(int x, int y)
{
	x*y;
}
int Div(int x, int y)
{
	x/y;
}

首先要明确函数指针数组的定义,它是一个数组则,应该写为

pf[]

然后数组中存储的元素是指针

*pf[]

该指针指向一个函数,函数的入参为int,int,返回值为int
所以可以定义为

int (*pf[])(int, int) = {Add, Sub, Mul, Div};

回调函数

 回调函数就是一个通过函数指针调用的函数,如果把函数的指针(地址)作为参数传递给另外一个函数,当这个指针被用来调用其所指向的函数时,就可以说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外的一方调用的,对于该事件或条件进行响应。

若存在以下结构体

typedef struct stu
{
    char name[20];
    int num;
    double score;
}Stu;

若存在一组Stu的结构体数组,想要实现按照不同的成员变量大小进行排序,则可以这样表示

void swap(void* arr1, void* arr2, size_t sz)
{
    int i = 0;
    for(i = 0; i<sz; i++)
    {
        char temp = *((char*)arr1 + i);
        *((char*)arr1 + i) = *((char*)arr2 + i);
        *((char*)arr2 + i) = temp;
    }
}

int cmp_by_name(void* arr1,void* arr2)
{
    return strcmp(((Stu*)arr1)->name,((Stu*)arr2)->name);
}

int cmp_by_num(void* arr1,void* arr2)
{
    return ((Stu*)arr1)->num - ((Stu*)arr2)->num;
}

int cmp_by_score(void* arr1,void* arr2)
{
    return ((Stu*)arr1)->score > ((Stu*)arr2)->score;
}

void bubble_sort(void* arr, size_t num, size_t sz, int (*cmp)(void*, void*))
{
    int i = 0;
    int j = 0;
    for(i = 0; i < num - 1; i++)
    {
        for(j = 0; j < num - i - 1; j++)
        {
            if(cmp((char*)arr+j*sz, (char*)arr+(j+1)*sz) > 0)
            {
                swap((char*)arr+j*sz, (char*)arr+(j+1)*sz, sz);
            }
        }
    }
}

void print(Stu* s, size_t size)
{
    for(int i = 0; i<size; i++)
    {
        printf("name:%-10s, num:%3d, score:%3.1f\n", s[i].name, s[i].num, s[i].score);
    }
}

// void test()
// { 
//     Stu student[MAX]={{"Alice", 24, 87.5f}, {"Zack", 16, 92.5f}, {"Sana", 34, 75.8f}};
//     print(student, sizeof(student)/sizeof(student[0]));
//     bubble_sort(student, sizeof(student)/sizeof(student[0]), sizeof(student[0]), cmp_by_score);
//     print(student, sizeof(student)/sizeof(student[0]));
// }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值