数组的误用

简介: 原文地址: Teaching C++ Badly: How to Misuse Arrays   我上次写了篇文章列举了我所看到的一些不好的C++教学,并且承诺详细地解释这些技术。

原文地址: Teaching C++ Badly: How to Misuse Arrays

 

我上次写了篇文章列举了我所看到的一些不好的C++教学,并且承诺详细地解释这些技术。这篇就是其中的第一篇。 我见到有归因于Trenchard More(*定义了More Array Theory)的断言,说数组是所有数据结构中最基本的一个。 事实上几乎没有哪个在世的程序员没有使用过数组。如果没有足够的证据,想象一下数组是一种线性寻址机制的抽象,它是所有计算机硬件通常使用的最基础的部分。

 

线性寻址的想法认为计算机的内存由一组具有连号地址的存储单元构成。通过使用普通的整数来计算存储单元的地址,我们可以很容易从“数组的第几个元素”的概念出发找到相应的地址。

 

因为这种数组概念与底层地址概念上的联系,数组操作在所有支持它的编程语言中都很相似。 一个数组有许多元素,每个元素都有一个下标或索引,并且这些索引数都是连续的整数。 有些语言从0开始索引,另外一些则从1开始,也有少数一些可以从任意位置开始,这关乎于语言间的差异性。

 

大多数支持数组的语言都会阻止在数组建立后修改数组的大小。 C与C++要求的更多: 它们要求数据的大小必须在编译时明确。不然,程序必须建立一个动态数组,它虽然和简单的数组有类似的属性,在一些细节上还是有所不同。 即便是这些动态数组,也不能在创建后修改大小。

 

现在让我们来考虑一下程序如何使用数组。大部分的程序员有三种完全不同的方式使用数组。有时这三件事是交错的,但通常一次只做一件:
 1. 存储数据
 2. 访问数据
 3. (可选)取出所有数据
 
第2步包括重新安排数据, 或许是排序。 第3步包括打印数据。

 

举个例子,考虑一个程序来计算素数。 每次它测试一个数字以检查它是不是一个素数,它也许使用在数组中已经存储的数据(第2步).每次它发一个素数,就会将它存储到一个数组中 (第1步). 最后,它也许要将所有找到数据打印出来 (第3步). 在这里,第1步和第2步是交错进行的,第3步有可能交错,也有可能独力运行。(*取决于边找找输出,还是最后一次性输出。)

 

当程序按这三步来操作数组时,必须面对两个问题。首先,如果编程语言要求程序员在创建数组时就冻结它的大小,那么程序员必须知道需要多少元素。再者,即使程序员已经知道元素数量,固定的大小意味着在程序运行的大部分情况下,数组中总是有元素的值是没有意义的。

这些限定就引出两个一般性的编程实践,以我的观点来看,它们比学习如何编程要难得多:
    *推测数组应当需要多少元素
    *使用一个辅助变量来追踪使用了多少元素
 
例如,让我们回到之前假设的素数程序. 你应当看到过许多程序以类似下面的方式开始:
  int primes[1000]; // 我们不需要超过1000个素数
  primes[0] = 2; // 第一个素数是2
  n = 1; // 目前为止我们已经有一个素数 

 

这个程序推测出我们会发现的素数数量(1000),并且使用一个辅助变量(n)来追踪我已经得到了多少素数。当我们得到一个新的素数时,程序会执行:
primes[n++] = prime;

 

如果程序经过认真思考,就会使用检查是否n>=1000的语句,否则程序就可能因为溢出而crash.

 

从教学的观点来看这个程序很差,三个理由:
    *它为程序的能力强加了一个限制 -- 它不能计算超过指定数量的素数。
    *在只需要一个变量时却使用了两个(primes和n),因此使得代码远比它所需要的复杂。
    *它在分配数组时浪费了资源。 

 

标准的vector模板可以避免这三个问题。对于上面的例子,我们可以使用如下代码重写:
 vector<int> primes; // primes开始并没有任何元素
 primes.push_back(2); //第一个素数是2

 

在即将发布的C++标准,我们可以将这个例子简化为一句话:
 vector<int> primes = {2};

当得到一个新的素数时,我们可以通过下面的语句来追加:
 primes.push_back(prime);

 

这个语句会将primes的大小增加1,追加了一个变量prime的复本到primes中. 实际上,它是将prime压到primes的背后。这样我们就处理掉了之前三个问题中的两个:1.使用vector替代数据移除了在元素数量上的约束。 2.只需要使用一个变量 (不再需要额外的辅助变量)。

当然,这并没有解决资源浪费的问题,因为vector在背后也分配了超出数组所需要的内存。然而现在是程序在浪费内存,而不是程序员。而且这种浪费让程序员有机会在不彻底重写代码的情况下,在多种空间与时间的权衡中进行选择。相对于教初学者如何写不必要的晦涩代码,我们可以教他们写合适的代码,然后在代码可以工作后,再来判断如何或者是否需要进行优化。

 

简言之,程序员往往乐于使数组在程序执行过程中增长。C/C++中内建的数据并不能达到要求,而vector可以做到。所以使得vcetor对教学目的而言更为有用,特别是面对初学者的时候。 此外,数组并不能限制学生撰写本质上不值得的代码。相反地,学生可以获得一个良好的习惯:首先让程序可以运行,然后就只需要思考如何进行优化。

目录
相关文章
|
2天前
|
存储 人工智能 编译器
【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节
【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节
15 1
|
9天前
|
存储 索引 Python
什么是数组,什么是对象,并说出他们的区别
什么是数组,什么是对象,并说出他们的区别
15 6
|
5月前
|
人工智能 编译器 测试技术
一次性搞清数组指针、指针数组---从多维数组的本质上刨析(让你顿悟版)
一次性搞清数组指针、指针数组---从多维数组的本质上刨析(让你顿悟版)
15 0
|
10月前
|
编译器 C语言 C++
C语言数组越界造成的死循环例子,当你得到了这个意想不到的结果的时候,你肯定不知道为什么,看你还敢不敢越界访问数组了
C语言数组越界造成的死循环例子,当你得到了这个意想不到的结果的时候,你肯定不知道为什么,看你还敢不敢越界访问数组了
|
存储 编译器 C语言
浅识C语言中那些操作符(保证足够详细)
浅识C语言中那些操作符(保证足够详细)
45 0
浅识C语言中那些操作符(保证足够详细)
|
存储 Java
Java面向对象(3.1)--方法的重载,可变个数的形参,值传递机制,递归
Java面向对象(3.1)--方法的重载,可变个数的形参,值传递机制,递归
92 0
如何避免数组下标越界
数组下标越界真的是开发过程中的痛,除了在开发过程中各种判断是否设置,是否为空,还有其他优雅的办法解决吗?
113 0
|
编译器 C语言 C++
【C 语言】数组作为参数退化为指针问题 ( 问题描述 | 从编译器角度分析该问题 | 出于提高 C 语言执行效率角度考虑 | 数组作为参数的推荐方案 )
【C 语言】数组作为参数退化为指针问题 ( 问题描述 | 从编译器角度分析该问题 | 出于提高 C 语言执行效率角度考虑 | 数组作为参数的推荐方案 )
135 0
【C 语言】数组作为参数退化为指针问题 ( 问题描述 | 从编译器角度分析该问题 | 出于提高 C 语言执行效率角度考虑 | 数组作为参数的推荐方案 )
|
存储 安全 C语言
【C 语言】字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )
【C 语言】字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )
86 0
【C 语言】字符串模型 ( 字符串翻转模型 | 借助 递归函数操作 逆序字符串操作 | 引入线程安全概念 )
|
C语言 C++
你想知道的数组易错知识都在这了-C
你想知道的数组易错知识都在这了-C
206 0
你想知道的数组易错知识都在这了-C