Chinaunix首页 | 论坛 | 博客
  • 博客访问: 7807395
  • 博文数量: 701
  • 博客积分: 2150
  • 博客等级: 上尉
  • 技术积分: 13233
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-29 16:28
个人简介

天行健,君子以自强不息!

文章分类

全部博文(701)

文章存档

2019年(2)

2018年(12)

2017年(76)

2016年(120)

2015年(178)

2014年(129)

2013年(123)

2012年(61)

分类: C/C++

2013-06-21 23:18:07

C++ 语言提供了两种类似于 vector 和迭代器类型的低级复合类型——数组和指针。
与 vector 类型相似,数组也可以保存某种类型的一组对象;
而它们的区别在于,
数组: 长度是固定的。数组一经创建,就不允许添加新的元素。
指针: 可以像迭代器一样用于遍历和检查数组中的元素。

现代 C++ 程序应尽量使用 vector 和迭代器类型,而避免使用低级的数组和指针。
设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。

4.1. 数组
数组是由类型名、标识符和维数组成的复合数据类型,
类型名规定了存放在数组中的元素的类型,
维数则指定数组中包含的元素个数。


数组定义中的类型名可以是内置数据类型或类类型;
除引用之外,数组元素的类型还可以是任意的复合类型。

4.1.1. 数组的定义和初始化
数组的维数必须用值大于等于1 的常量表达式定义。
此常量表达式只能包含整型字面值常量、枚举常量或者用常量表达式初始化的整型 const 对象。
非 const 变量以及要到运行阶段才知道其值的 const变量都不能用于定义数组的维数。
数组的维数必须在一对方括号 [] 内指定:

// both buf_size and max_files are const
const unsigned buf_size = 512, max_files = 20;
int staff_size = 27;             // nonconst
const unsigned sz = get_size();  // const value not known until run time
char input_buffer[buf_size];     // ok: const variable
string fileTable[max_files + 1]; // ok: constant expression
double salaries[staff_size];     // error: non const variable
int test_scores[get_size()];     // error: non const expression
int vals[sz];                    // error: size not known until run time


1. 显式初始化数组元素
在定义数组时,可为其元素提供一组用逗号分隔的初值,这些初值用花括号{}括起来,
称为初始化列表:
const unsigned array_size = 3;
int ia[array_size] = {0, 1, 2};

如果没有显式提供元素初值,则数组元素会像普通变量一样初始化:
? 在函数体外定义的内置数组,其元素均初始化为 0。
? 在函数体内定义的内置数组,其元素无初始化。
? 不管数组在哪里定义,如果其元素为类类型,则自动调用该类的默认构造
  函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化。
? 除非显式地提供元素初值,否则内置类型的局部数组的元素没有初始化。
  此时,除了给元素赋值外,其他使用这些元素的操作没有定义。
? 显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度:
  int ia[] = {0, 1, 2}; // an array of dimension 3


2. 特殊的字符数组
字符数组既可以用一组由花括号括起来、逗号隔开的字符字面值进行初始化;

也可以用一个字符串字面值进行初始化。然而,要注意这两种初始化形式并不完全相同,
字符串字面值包含一个额外的空字符(null)用于结束字符串。
当使用字符串字面值来初始化创建的新数组时,将在新数组中加入空字符:
char ca1[] = {'C', '+', '+'};         // no null
char ca2[] = {'C', '+', '+', '\0'};   // explicit null
char ca3[] = "C++";                   // null terminator added automatically


3. 不允许数组直接复制和赋值
与vector 不同,
一个数组不能用另外一个数组初始化,
也不能将一个数组赋值给另一个数组;

4. 警告:数组的长度是固定的
与 vector 类型不同,数组不提供 push_back 或者其他的操作在数组中添加新元素,
数组一经定义,就不允许再添加新元素。
如果必须在数组中添加新元素,程序员就必须自己管理内存:
要求系统重新分配一个新的内存空间用于存放更大的数组,
然后把原数组的所有元素复制到新分配的内存空间中。

4.1.2. 数组操作
与vector 元素一样,数组元素可用下标操作符来访问,数组元素也是从 0 开始计数;

1. 检查数组下标值
正如 string 和 vector 类型,
程序员在使用数组时,也必须保证其下标值在正确范围之内,即数组在该下标位置应对应一个元素。
除了程序员自己注意细节,并彻底测试自己的程序之外,没有别的办法可防止数组越界;

4.2. 指针的引入
可用下标或指针来遍历数组。
指针是指向某种类型对象的复合数据类型,是用于数组的迭代器:指向数组中的一个元素。
在指向数组元素的指针上使用解引用操作符 *和自增操作符 ++;

4.2.1. 什么是指针
指针的概念很简单:指针用于指向对象;
具体来说,指针保存的是另一个对象的地址:
string s("hello world");
string *sp = &s; // sp holds the address of s

4.2.2. 指针的定义和初始化
每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。
例如,一个 int 型指针只能指向 int 型对象。

1. 指针变量的定义
C++ 语言使用 * 符号把一个标识符声明为指针:
vector *pvec;       // pvec can point to a vector
int         *ip1, *ip2;  // ip1 and ip2 can point to an int
string      *pstring;    // pstring can point to a string
double      *dp;         // dp can point to a double

2. 指针可能的取值
一个有效的指针必然是以下三种状态之一:
保存一个特定对象的地址;
指向某个对象后面的另一对象;
或者是0 值。若指针保存0 值,表明它不指向任何对象。
未初始化的指针是无效的,直到给该指针赋值后,才可使用它。

3. 避免使用未初始化的指针
很多运行时错误都源于使用了未初始化的指针。

4. void* 指针
C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址:
double obj = 3.14;
double *pd = &obj;
// ok: void* can hold the address value of any data pointer type
void *pv = &obj;      // obj can be an object of any type
pv = pd;              // pd can be a pointer to any type

void* 表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。
void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递
void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不允许使用
void* 指针操纵它所指向的对象。

4.2.3. 指针操作
1. 生成左值的解引用操作
解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值:
*sp = "goodbye"; // contents of s now changed

2. 指针和引用的比较
虽然使用引用(reference)和指针都可间接访问另一个值,但它们之间有两个重要区别。
第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。
第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,
      而并不是使引用与另一个对象关联。
引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。

考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:
int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2; // pi now points to ival2
赋值结束后,pi 所指向的 ival 对象值保持不变,赋值操作修改了 pi 指针的值,使其指向另一个不同的对象。

现在考虑另一段相似的程序,使用两个引用赋值:
int &ri = ival, &ri2 = ival2;
ri = ri2; // assigns ival2 to ival
这个赋值操作修改了 ri 引用的值 ival 对象,而并非引用本身。赋值后,
这两个引用还是分别指向原来关联的对象,此时这两个对象的值相等。

3. 指向指针的指针
指针本身也是可用指针指向的内存对象。指针占用内存空间存放其值,因此指针的存储地址可存放在指针中

4.2.4. 使用指针访问数组元素
1. 指针的算术操作
与其使用下标操作,倒不如通过指针的算术操作来获取指定内容的存储地址

2. 指针是数组的迭代器

4.2.5. 指针和 const 限定符
1. 指向 const 对象的指针
到目前为止,我们使用指针来修改其所指对象的值。
但是如果指针指向const 对象,则不允许用指针来改变其所指的 const 值。为了保证这个特性,
C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:

const double *cptr;          // cptr may point to a double that is const

这里的 cptr 是一个指向 double 类型 const 对象的指针,
const 限定了cptr 指针所指向的对象类型,而并非 cptr 本身。
也就是说,cptr 本身并不是const。
在定义时不需要对它进行初始化,如果需要的话,允许给 cptr 重新赋值,使其指向另一个 const 对象。
但不能通过 cptr 修改其所指对象的值:

*cptr = 42; // error: *cptr might be const

把一个 const 对象的地址赋给一个普通的、非 const 对象的指针也会导致编译时的错误:
const double pi = 3.14;
double *ptr = π // error: ptr is a plain pointer
const double *cptr = π // ok: cptr is a pointer to const

不能使用 void* 指针保存 const 对象的地址,而必须使用 const void* 类型的指针保存 const 对象的地址:
const int universe = 42;
const void *cpv = &universe; // ok: cpv is const
void *pv = &universe;        // error: universe is const

允许把非 const 对象的地址赋给指向 const 对象的指针,例如:
double dval = 3.14; // dval is a double; its value can be changed
cptr = &dval;       // ok: but can't change dval through cp

2. const 指针
除指向 const 对象的指针外,C++ 语言还提供了 const 指针——本身的值不能修改:
int errNumb = 0;
int *const curErr = &errNumb; // curErr is a constant pointer

3. 指向 const 对象的 const 指针
还可以如下定义指向 const 对象的 const 指针:
const double pi = 3.14159;

// pi_ptr is const and points to a const object
const double *const pi_ptr = π
本例中,既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向(即 pi_ptr 中存放的地址值)。
可从右向左阅读上述声明语句:
“pi_ptr 首先是一个 const 指针,指向 double 类型的 const 对象”。

4. 建议:理解复杂的 const 类型的声明
阅读 const 声明语句产生的部分问题,源于 const 限定符既可以放在类型前也可以放在类型后:
string const s1; // s1 and s2 have same type,
const string s2; // they're both strings that are const

用 typedef 写 const 类型定义时,const 限定符加在类型名前面容易引起对所定义的真正类型的误解:

string s;
typedef string *pstring;
const pstring cstr1 = &s;         // written this way the type is obscured
pstring const cstr2 = &s;         // all three decreations are the same type
string *const cstr3 = &s;         // they're all const pointers to string

把 const 放在类型 pstring 之后,
然后从右向左阅读该声明语句就会非常清楚地知道 cstr2 是 const pstring 类型,
即指向 string 对象的 const 指针。
不幸的是,大多数人在阅读 C++ 程序时都习惯看到 const 放在类型
前面。于是为了遵照惯例,只好建议编程时把 const 放在类型前面。
但是,把声明语句重写为置 const 于类型之后更便于理解。

5. 表 4.1. 操纵 C 风格字符串的标准库函数
strlen(s);        返回 s 的长度,不包括字符串结束符 null
strcmp(s1, s2);   比较两个字符串 s1 和 s2 是否相同。
                  若 s1 与 s2 相等,返回 0;
                  若 s1 大于 s2,返回正数;
                  若 s1 小于 s2,则返回负数
strcat(s1, s2);   将字符串 s2 连接到 s1 后,并返回 s1
strcpy(s1, s2);   将 s2 复制给 s1,并返回 s1
strncat(s1,s2,n)  将 s2 的前 n 个字符连接到 s1 后面,并返回 s1
strncpy(s1,s2, n) 将 s2 的前 n 个字符复制给 s1,并返回 s1

4.3.1. 创建动态数组
1.动态数组的定义
数组变量通过指定类型、数组名和维数来定义。
而动态分配数组时,只需指定类型和数组长度,不必为数组对象命名,
new 表达式返回指向新分配数组的第一个元素的指针:

int *pia = new int[10]; // array of 10 uninitialized ints

2. 初始化动态分配的数组
动态分配数组时,
如果数组元素具有类类型,将使用该类的默认构造函数实现初始化;
如果数组元素是内置类型,则无初始化:

string *psa = new string[10]; // array of 10 empty strings
int *pia = new int[10]; // array of 10 uninitialized ints

也可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化:

int *pia2 = new int[10] (); // array of 10 uninitialized in

3. const 对象的动态数组
如果我们在自由存储区中创建的数组存储了内置类型的 const 对象,则必须为这个数组提供初始化:
因为数组元素都是 const 对象,无法赋值。
实现这个要求的唯一方法是对数组做值初始化:

// error: uninitialized const array
const int *pci_bad = new const int[100];


// ok: value-initialized const array
const int *pci_ok = new const int[100]();

4. 动态空间的释放
动态分配的内存最后必须进行释放,否则,内存最终将会逐渐耗尽。
如果不再需要使用动态创建的数组,程序员必须显式地将其占用的存储空间返还给程序的自由存储区。

C++ 语言为指针提供 delete [] 表达式释放指针所指向的数组空间:
delete [] pia;

阅读(1901) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~