《C和C++代码精粹》——2.7 指针和一维数组

简介:

本节书摘来自异步社区出版社《C和C++代码精粹》一书中的第2章,第2.7节,作者: 【美】Chuck Allison,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.7 指针和一维数组

C和C++代码精粹
在程序清单2.7中,会注意到在传递数组 s 时并没有使用它的地址,这是因为C和C++在大多数表达式中把数组名转换成指向它第一个元素的指针。自1984年以来,我已经向成百上千的学生讲授了C和C++,我注意到了指针和数组,特别是指针和多维数组之间的关系造成很多迷惑。

这样说似乎很奇怪,但是C++确实不支持数组,至少C++不像支持第一类数据类型如整型或者甚至结构体那样支持数组。考虑以下的语句:

int i=1,j;
int a[4]={0,1,2,3},b[4];
struct pair {int j,int y;};
pair p={1,2},q;
j=i;          //OK:整型赋值
q=p;          //OK:结构体赋值
b=a;          //不能这样做

并不是所有有关数组的操作都是合法的。我们可以进行以下操作,但是它并不是一个“真实”的赋值:

int a[4]={0,1,2,3},*p;
p=a;                 /*只在p中存储了a[0]的地址*/

除了在声明中或者当一个数组名是sizeof运算符或&运算符的操作数之外,编译器总是把数组名解释成指向它的第一个元素的指针。可以将这个原则表达为:

a==&a[0]

或者等价于:

*a==a[0]

使用指针运算的规则,那么当把一个整型变量i和一个数组名相加,结果就得到指向数组第i个元素的指针,也就是:

a+i==&a[i]

或者,像我喜欢的表达方式一样:

重要的指针原则 2:*(a+i)==a[i]

程序清单2.8中的程序阐述了原则2以及准备步骤。

由于所有的数组下标是真正的指针运算,可以使用表达式i[a]代替a[i]。这些可直接从原则2中得到:

a[i]==*(a+i)==*(i+a)==i[a]

当然,任何使用了这样极端错误的表达的程序都会被中断而不被执行,而且程序员也会受到严厉的谴责。然而,使用相反的下标也不是完全没有道理,如果一个指针p传递一个数组,就可以使用表达式p[-1]重新得到在*p之前的元素,由于:

p[-1]==*(p-1)

程序清单 2.9极为全面地涵盖了指针和数组符号的结合,它也使用了一个关于数组中元素个数的有用公式:

size_t n=sizeof a/sizeof a[0];

虽然可以在除数中使用任何一个有效的下标,但0是最安全的,这是因为每个数组都有第0个元素。当然,这一习惯只有当原始的数组声明是在生存期内才适用。

对于那些愿意使用C风格字符串的人来说,一个遵循指针和数组符号概念之间相互作用的常用习惯是:

strncpy(s,t,n)[n]='\0';

程序清单2.8 说明数组名是指针

// array1.cpp: 用一个数组名作为一个指针  
#include <iostream>  
using namespace std;  

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

    cout << "sizeof a == " << sizeof a << endl;  
    cout << "sizeof p == " << sizeof p << endl;  
    cout << "p == " << p << ", &a[0] == " << &a[0] << endl;  
    cout << "*p == " << *p << ", a[0] == " << a[0] << endl;  

    p = a + 2;  
    cout << "p == " << p << ", &a[2] == " << &a[2] << endl;  
    cout << "*p == " << *p << ", a[2] == " << a[2] << endl;  
}  

//输出:  
sizeof a == 20  
sizeof p == 4  
p == 0x0012ff78, &a[0] == 0x0012ff78  
*p == 0, a[0] == 0  
p == 0x0012ff80, &a[2] == 0x0012ff80  
*p == 2, a[2] == 2

程序清单2.9 使用索引和指针传递数组

// array2.cpp: 使用索引和指针传递数组  
#include <iostream>  
using namespace std;  

main()  
{  
    int a[] = {0,1,2,3,4};  
    size_t n = sizeof a / sizeof a[0];  

    //使用数组索引打印  
    for (int i = 0; i < n; ++i)  
        cout << a[i] << ' ';  
    cout << endl;  
    //你甚至可以交替a和i(但自己别这么做!)  
    for (int i = 0; i < n; ++i)  
        cout << i[a] << ' ';  
    cout << endl;  

    //使用指针打印  
    int* p = a;  
    while (p < a+n)  
        cout << *p++ << ' ';  
    cout << endl;  

    //和指针一起使用索引符是好的:  
     p = a;  
    for (int i = 0; i < n; ++i)  
        cout << p[i] << ' ';  
    cout << endl;  

    //和数组一起使用指针符是好的:  
    for (int i = 0; i < n; ++i)  
        cout << *(a+i) << ' ';  
    cout << endl;  

    //使用指针向后打印:  
    p = a + n-1;  
    while (p >= a)  
        cout << *p-- << ' ';  
    cout << endl;  

    //写在下方的负数是允许的:   
    p = a + n-1;  
    for (int i = 0; i < n; ++i)  
        cout << p[-i] << ' ';  
    cout << endl;  
}  

//输出:  
0 1 2 3 4  
0 1 2 3 4  
0 1 2 3 4  
0 1 2 3 4  
0 1 2 3 4  
4 3 2 1 0  
4 3 2 1 0

这就把一个字符串拷贝到另一个字符串,同时确保没有溢出并且字符串没有被划上界限(假设n没有超限)—所有这些都在一个简短的语句当中实现。

在指针和数组名之间有另一个区别需要记住:一个数组名是一个不可改变的左值。这就意味着不能改变数组名对应的地址,就像下面的示例所尝试的那样:

int a[5],b[5],*p;
/*下面所有的都不合法*/
a++;
a=p+5;
b=a;

如果这样赋值,就会很轻易地丢失数组在内存中存储的位置(这可不是个好主意!)。

从字面上来说字符串是一组没有名称的字符,可以使用sizeof得到它们的大小并且甚至可以给它们添加下标(见程序清单2.10 和2.11)。注意在清单2.10中我的编译器把“hello”的每一次的出现都作为一个独立的对象,每次都返回不同的地址,有些编译器能够把具有相同字符的字符串“集中起来”以单个形式出现以节省空间。

程序清单2.10 说明一个字符串中的字符是一个匿名数组

// array3.cpp  
#include <iostream>  
using namespace std;  

main()  
{  
    char  a[] = "hello";  
    char* p = a;  

    cout << "a == " << &a << ", sizeof a == " << sizeof a << endl;  
    cout << "p == " << (void*)p << ", sizeof p == " << sizeof p << endl;  
    cout << "sizeof \"hello\" == " << sizeof "hello" << endl;  
    cout << "address of \"hello\" == " << (void*)"hello" << endl;  
    cout << "address of \"hello\" == " << (void*)"hello" << endl;  
}  

//输出:  
a == 0x0012ff84, sizeof a == 6  
p == 0x0012ff84, sizeof p == 4  
sizeof "hello" == 6  
address of "hello" == 0x004090d4  
address of "hello" == 0x004090f1

程序清单2.11 将字符串中的字符进行索引

// array4.cpp: 将字符串中的字符索引  
#include <iostream>  
using namespace std;  

main()  
{  
    for (int i = 0; i < 10; i += 2)  
        cout << "0123456789"[i];  
}  

//输出:  
02468

练习 2.1

已知如下声明:

int a[ ] = { 10, 15, 4, 25, 3, -4 };
int *p = &a[ 2 ];

下面表达式的结果是什么?

a.  *(p+1)    
b.  p[-1]       
c.  p-a         
d.  a[*p++]     
e.  *(a+a[2])

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

相关文章
|
6天前
|
C语言 C++ 开发者
深入探索C++:特性、代码实践及流程图解析
深入探索C++:特性、代码实践及流程图解析
|
16小时前
|
C++
【期末不挂科-C++考前速过系列P6】大二C++实验作业-模板(4道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P6】大二C++实验作业-模板(4道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P6】大二C++实验作业-模板(4道代码题)【解析,注释】
|
16小时前
|
Serverless C++ 容器
【期末不挂科-C++考前速过系列P5】大二C++实验作业-多态性(3道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P5】大二C++实验作业-多态性(3道代码题)【解析,注释】
|
16小时前
|
C++ 芯片
【期末不挂科-C++考前速过系列P4】大二C++实验作业-继承和派生(3道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P4】大二C++实验作业-继承和派生(3道代码题)【解析,注释】
|
16小时前
|
编译器 C++
【期末不挂科-C++考前速过系列P3】大二C++第3次过程考核(20道选择题&12道判断题&2道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P3】大二C++第3次过程考核(20道选择题&12道判断题&2道代码题)【解析,注释】
|
16小时前
|
C++
【期末不挂科-C++考前速过系列P2】大二C++第2次过程考核(20道选择题&10道判断题&3道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P2】大二C++第2次过程考核(20道选择题&10道判断题&3道代码题)【解析,注释】
|
1天前
|
存储 数据安全/隐私保护 C++
【期末不挂科-C++考前速过系列P1】大二C++第1次过程考核(3道简述题&7道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P1】大二C++第1次过程考核(3道简述题&7道代码题)【解析,注释】
|
1天前
|
编译器 C语言 C++
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
9 0
|
2天前
|
存储 C++
【C++模板】模板实现通用的数组
【C++模板】模板实现通用的数组
|
5天前
|
设计模式 编译器 数据安全/隐私保护
C++ 多级继承与多重继承:代码组织与灵活性的平衡
C++的多级和多重继承允许类从多个基类继承,促进代码重用和组织。优点包括代码效率和灵活性,但复杂性、菱形继承问题(导致命名冲突和歧义)以及对基类修改的脆弱性是潜在缺点。建议使用接口继承或组合来避免菱形继承。访问控制规则遵循公有、私有和受保护继承的原则。在使用这些继承形式时,需谨慎权衡优缺点。
17 1