C语言摘要 -- K&R笔记(3)

本文是关于《K&R》第五章《Pointer and Arrays》的摘要,详细讲解了指针作为数据类型的概念,指针与数组的关系,包括指针地址运算、字符串表示、指针数组、多维数组以及函数指针的用法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

个人博客:SHIZHZ's Blogsicon-default.png?t=LBL2https://shizhz.me/

本文是对《K&R》第五章《Pointer and Arrays》的摘要。

1. 指针是一种数据类型,其存储的内容是其它变量的内存地址,指针的合法值是合法内存地址或0(NULL)。一元操作符&与*用于指针操作:

&:该操作符作用于变量,可理解为‘address-of’, &p即表示p的地址,由于改操作符的结果是得到一个内存地址,所以改操作符只能用于内存中的变量与数组元素,不能用于表达式(expression),常量和寄存器变量(register修饰的变量);

*:该操作符作用于指针,可理解为‘the value pointed by this pointer',*p即表示指针p所指对象的值。

2. 指针与数组:

在C中指针与数组有紧密的联系,如果在C中声明了一个数组int a[5],那么编译器会分配一段连续的内存用来存储这5个int数组变量,而a就是整个内存块的起始地址,即第一个变量a[0]的地址,所以a 与 &a[0]是等价的,可以将其赋值给任何int类型的指针,如:

#include <stdio.h>

int main (int argc, char const* argv[])
{
    int a[5] = {1,2,3,4,5};
    int *p = a;
    if(a == &a[0]) printf("yes!\n");
    printf("%d, %d, %d\n", a[0], *a, *p);
    return 0;
}

编译后结果为:

$ ./a.out 
yes!
1, 1, 1

3. 指针地址运算:

1). 比较运算:因为指针的值是内存地址或0,所以算术比较运算符(==, !=, <=, >=, >, <)可用于指针,但通常只有在比较属于同一数组的元素地址时才有意义,可以通过此方式判断数组中元素的先后顺序;

2). 加法运算:指针可以与常量数值做加法运算,p + N表示当前p所指元素后面的第N个同类型元素地址,编译器通过指针p的数据类型知道每个元素的大小,例如int a[10], 那么a + 3表示&a[3];

3). 减法运算:指针与常量数值做减法运算时,其意义与加法类似但方向相反,例如int a[10], 那么(&a[9] - 5)表示&a[4];指针也可以与指针类型做减法,其结果表示两个地址之间包含了多少个同类型的变量:

#include <stdio.h>

int main (int argc, char const* argv[])
{
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("%lu\n", (&a[9] - a));
    return 0;
}

结果为:

$ ./a.out 
9

同理,减法运算通常也只有作用于属于同一数组中的元素时才有意义。

4. 字符串:

C中没有字符串类型,字符串即是char类型的指针,编译器内部表示为以字符'\0'结尾的字符串数组,字符'\0'用于帮助程序找到字符串的结尾。

5. 指针数组:即数组内每个元素是指向声明类型的指针,如char*argv[]中,argv[i]表示的是一个字符串。

6. 多维数组:以二维数组为例进行说明,int a[rows][cols]的声明中,cols必须显示声明大小,不管是否对该数组进行初始化;在函数的形参中,二维数组的cols也必须显示指定大小:

#include <stdio.h>

#define ROW 2
#define COLS 10

void print_mularray(int [][COLS] , int len);

int main (int argc, char const* argv[])
{
    int a[][COLS] = {
        {1,2,3,4,5,6,7,8,9,10},
        {-1,-2,-3,-4,-5,-6,-7,-8,-9,-10}
    };
    print_mularray(a, ROW);
    return 0;
}

void print_mularray(int a[][COLS], int len)
{
    int i = 0;
    for(; i < len; i ++)
    {
        int j = 0;
        for(; j < COLS; j ++)
            printf("a[%d][%d]: %d\n", i, j, a[i][j]);
    }
}

那么为什么在形参中和在数组声明中,必须指定cols的大小呢?看如下示例:

#include <stdio.h>

void f(int [][]);
int a[][];

int main (int argc, char const* argv[])
{
    return 0;
}

编译时错误信息:

$ gcc mularray.c 
mularray.c:3:1: 错误:数组元素的类型不完全
mularray.c:4:5: 错误:数组元素的类型不完全

编译器抱怨数组元素的类型不完全!首先看一下二维数组的内存布局:从程序视角我们可以将内存看成一个由字节组成的一维数组,其存储二维数组的方式是先存放第一列,再存放第二列并依次类推,如果我们有二维数组int a[2][5] = {{1,2,3,4,5}, {6,7,8,9,0}},那么其在内存中布局为:

1234567890

那么如果在int a[rows][cols]的数组中求元素a[i][j]的值,其寻址方式为:a + i * sizeof(int [cols]) / sizeof(int) + j,即:a + i * cols + j;可知编译器必须知道cols的值,才能进行寻址,换句话说,编译器必须知道二维数组中每行的大小,才能正确的进行内存分配,而该值是通过cols来指定的。在上例int a[][]的声明中,编译器能够解析出数组a[]中的每个元素是一个int数组,但是不知道该数组的大小,所以抱怨类型不完全。再看下例:

#include <stdio.h>

#define ROW 2
#define COLS 10

void print_mularraies(int (*)[]);

int main (int argc, char const* argv[])
{
    int a[][COLS] = {
        {1,2,3,4,5,6,7,8,9,10},
        {-1,-2,-3,-4,-5,-6,-7,-8,-9,-10}
    };
    print_mularraies(a);
    return 0;
}

void print_mularraies(int (*a)[])
{
    printf("%d", a[1][3]);
    printf("%d, %d\n", (*a)[1], (*(a + 1))[1]);
}

编译时错误信息:

$ gcc -Wall pointer.c
gcc -Wall pointer.c 
pointer.c: 在函数‘print_mularraies’中:
pointer.c:22:5: 错误:对未指定边界的数组的使用无效
pointer.c:23:5: 错误:对未指定边界的数组的使用无效

原理类似,可以理解为:编译器在解析函数的形参int (*a)[]时,知道a是一个指针,指向一个数据类型是int []的对象,但是由于缺乏数组大小的声明,编译器无法获知每个对象占用多少内存,便无法进行a + 1这样的运算;同理a[1][3]等同于(*(a + 1))[3],错误相同。在函数的形参中声明数组的大小即可:

#include <stdio.h>

#define ROW 2
#define COLS 10

void print_mularraies(int (*)[]);

int main (int argc, char const* argv[])
{
    int a[][COLS] = {
        {1,2,3,4,5,6,7,8,9,10},
        {-1,-2,-3,-4,-5,-6,-7,-8,-9,-10}
    };
    print_mularraies(a);
    return 0;
}

void print_mularraies(int (*a)[COLS])
{
    printf("%d\n", a[1][3]);
    printf("%d, %d\n", (*a)[1], (*(a + 1))[1]);
}

编译运行结果正确:

$ ./a.out 
-4
2, -2


7. 指针数组与多维数组:

同一数据类型的指针数组与多维数组在使用上类同,但在内存分配上并不一样。通过对多维数组的声明,编译器明确知道需要为其分配多少内存,如:

int [2][100];

编译器会为其分配sizeof(int) * 2 * 100个字节,而对于:

int *[100];

编译器只会初始化一个有100个int指针类型的数组,每个指针指向哪里则并没有定义。

一个字符串数组的内存布局示例如下:

8. 函数指针:

C中函数不是变量,但是可定义指向函数的指针,并将其用于赋值、传递给函数以及从函数返回,函数指针的声明格式为:

return-type (*) (arg-list)

使用方式见如下示例:

#include <stdio.h>

int runner(int (*)(int, int), int, int);
int sum(int, int);
int sub(int, int);

int main (int argc, char const* argv[])
{
    int i = 10, j = 5;
    int r1, r2;
    r1 = runner((int (*)(int, int))sum, i, j);
    r2 = runner((int (*)(int, int))sub, i, j);
    printf("sum: %d; sub: %d\n", r1, r2);
    return 0;
}

int runner(int (*f)(int, int), int a, int b)
{
    return (*f)(a, b);
}

int sum(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}

运行结果为:

$ ./a.out 
sum: 15; sub: 5


 ______________ 
< 本章终了 >
 -------------- 
        \   ^__^
          \  (oo)\_______
              (__)\       )\/\
                  ||------w |
                  ||        ||

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值