C++学习笔记4:数组和指针

作者: 云中布衣   分类:  学习笔记    热度: (325℃)   时间: 2017-6-24 10:48   标签: #始于C++而不止于C++    

C++语言提供了两种类似于vector和迭代器类型的低级符合类型——数组和指针。与vector类型相似,数组也可以保存某种类型的一组对象;而它们的区别在于,数组的长度是固定的。数组一经创建,就不允许添加新的元素。指针可以向迭代器一样用于遍历和检查数组中的元素。现代C++程序应尽量使用vector和迭代器类型,而避免使用低级的数组和指针。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。

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;
const unsigned sz = get_size();//const value not konw until run time
char input_buffer[buf_size];//ok
string fileTable[max_files+1];//ok
double salaries[staff_size];//error
int test_scores[get_size()];//error
int vals[sz];//error
数组的初始化:在定义数组时,可为其元素提供一组用逗号分隔开的初值,这些初值用花括号{}括起来,称为初始化列表。
const unsigned array_size = 3;
int ia1[array_size] = {0, 1, 2};
int ia2[] = {0, 1, 2};//Display initialized don't need dimensions of array
如果没有显式的提供元素初值,则数组元素会像普通变量一样初始化。
  • 在函数体外定义的内置数组,其元素均初始化为0;
  • 在函数体内定义的内置数组,其元素无初始化;
  • 不管数组在哪里定义,如果其元素是类类型,则自动调用该类的默认构造函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显示的初始化。

对于字符数组而言,既可用一组由花括号开括起来、逗号隔开的字符字面值进行初始化,也可用一个字符串字面值进行初始化。需要注意的是字符串字面值包含一个额外的空字符(null)用于结束字符串。当使用字符串字面值来初始化创建的新数组时,将自动在新数组末尾处加入空字符。而使用字符字面值初始化字符数组时则需要手动添加结束字符串的空字符。

char ca1[] = {'C', '+', '+'};
char ca2[] = {'C', '+', '+',  '\0'}; //explicit null
char ca3[] = "C++";//null terminator added automaticlly
与vector不同,一个数组不能用另一个数组初始化,也不能将一个数组赋值给另一个数组,这些操作都是非法的。数组的长度是固定的,它不提供push_back或者其他的操作在数组中添加新元素,数组一经定义,就不允许再添加新元素。

数组的操作:与vector元素一样,数组元素可用下标操作符来访问,数组元素也是从0开始计数。在用下标访问元素时,vector使用vector::size_type作为下标类型,而数组下标的正确类型则是size_t。

const size_t array_size = 7;
int ia1[] = {0, 1, 2, 3, 4, 5 ,6};
int ia2[array_size];// local array, elements uninitialized

//copy elements from ia1 into ia2
for(size_t ix = 0; ix != array_size; ++ix)
    ia2[ix] = ia1[ix];

for(size_t ix = 0; ix != array_size; ++ix)
    cout << ia2[ix]<< " ";
cout << endl;
return 0;
正如string和vector类型,程序员在使用数组时,必须保证其下标值在正确的范围之内,即数组在该下标位置应对应一个元素。除了程序员自己注意细节,并彻底测试自己的程序之外,没有别的办法可以防止数组越界。导致安全问题的最常见的原因是所谓"缓冲区溢出(buffer overflow)"错误。当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致此类错误。

2.指针

vector的遍历可使用下标或迭代器实现,同理,也可用下标或指针(pointer)来遍历数组。指针是指向某种类型对象的复合数据类型,是用于数组的迭代器:指向数组的第一个元素。在指向数组元素的指针上使用解引用操作符*(dereference opeator)和自增操作符++(increment operator),与迭代器上的用法类似。对指针进行解引用,可获得该指针所指向对象的值。而当指针做自增操作时,则移动指针使其指向数组的下一个元素。

简单的来说指针用于指向对象,具体的来说指针保存的是另一个对象的地址:

string s("hello world");
string *sp = &s; // sp holds the address of s
其中&s的&符号是取地址(address-of)操作符,当此操作符用于一个对象上,返回的是该对象的存储地址。取地址操作符只能用于左值,因为只有当变量用作左值时,才能取其地址。同样地,由于用于vector类型、string类型或内置数组的下标操作和解引用操作生成左值,因此可对这两种操作的结果做取地址操作,这样即可获取某一特定对象的地址。每一个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。例如,一个int型指针只能指向int型对象。

指针的定义:C++语言使用*符号把一个标识符声明为指针。其中符号*可以紧挨着类型名右边放置,这种风格强调这个声明语句是一个指针;也可以把符号*靠近标识符左边放置,这种风格强调对象是一个指针。关于指针的声明不能说哪种声明风格是唯一正确的方式,重要的是选择一种风格并持续使用。一个有效指针必然是一下三种状态之一:保存一个特定对象的地址;指向某个对象后面的另一对象;或者是0值。若指针保存0值,表明它不指向任何对象。未初始化的指针是无效的,直到给该指针赋值后,才可以使用它。很多运行时错误都源于使用了未初始化的指针,要避免使用未初始化的指针。

vector<int> *pvec;
int *ip1, *ip2;
string *pstring;
double *dp = 0;

指针初始化和赋值操作的约束:对指针进行初始化或赋值只能使用以下四种类型的值。

1)0值常量表达式,例如,在编译时可获得0值的整型const对象或字面值常量0

2)与指针定义时所指明的类型相匹配的对象的地址

3)另一对象之后的下一地址

4)同类型的另一个有效指针

除了使用数值0或在编译值为0的const量外,还可以使用C++语言从C语言继承下来的预处理器变量NULL,该变量在cstdlib头文件中定义,其值为0。如果在代码中使用这个预处理器变量,则编译时会自动被数值0替换。C++还提供了一种特殊的指针类型void*,它可以保存任何类型对象的地址,void*表明该指针与一地址相关,但不清楚存储在此地址上的对象的类型。

double obj = 3.14;
double *pd = &obj;
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*指针操纵它所指向的对象。

指针的操作:指针提供间接操纵其所指对象的功能。与对迭代器进行解引用操作一样,对指针进行解引用可访问它所指的对象,*操作符(解引用操作符)将获取指针所指的对象。

string s("hello world");
string *sp = &s; // sp holds the address of s
cout << *sp; //prints hello world
*sp = "goodbye";// contents of s now changed
string s2 = "some value";
sp = &s2;//sp now points to s2
对sp进行解引用将获得s的值,然后用输出操作符输出该值。解引用操作符返回指定对象的左值,利用这个功能可以修改指针所指向的对象。给指针直接赋值即可修改指针的值,不需要对指针进行解引用。

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

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

int ival = 1024;
int *pi = &ival; // pi points to an int 
int **ppi = &pi; //ppi points to a pointer to int

使用指针访问数组元素:C++语言中,指针和数组密切相关。特别是在表达式中使用数组名时,该名字会自动转换为指向数组的第一个元素的指针。我们可以通过对指针的算术操作(pointer arithmetic)来获取指定内容的存储地址。使用指针的算术操作在指向数组某个元素的指针上加上(或减去)一个整型数值就可以计算出指向数组另一个元素的指针值。指针的算术操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一个单元时才是合法的。如果指针指向一个对象,我们还可以在指针上加1从而获取指向相邻的下一个对象的指针。

此外只要两个指针指向同一个数组或有一个指向该数组末端的下一单元,C++还支持对两个指针做减法操作。两个指针相减操作的结果是标准库类型(library type)ptrdiff_t的数据。与size_t类型一样,ptrdiff_t 也是一种与机器相关的类型,在cstddef头文件中定义。size_t是unsigned类型的,而ptrdiff_t则是signed整型。

ip = ia;
int *ip2 = ip +4;
ptrdiff_t n = ip2 - ip; //ok:distance between the points

在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新的指针。我们已经看到,在表达式中使用数组名,实际上使用的是指向数组第一个元素的指针,使用下标访问数组,实际上是使用下标访问指针。

int ia[] = {0, 2, 4, 6, 8};
int i = ia[0]; // ia points to the first element in ia
int *p = &ia[2];
int j = p[1]; // ok:p[1] equivalent to *(p+1) , is the same  element as ia[3]
int k = p[-2]; // ok:p[-2] is the same element as ia[0]
vector类型提供的end操作,将返回指向超出vector末端位置的一个迭代器。这个迭代器被用作哨兵,来控制处理vector中元素的循环。类似地,我们也可以计算数组的超出末端指针的值,不能对其进行解引用操作,但可以与其他指针比较,或者用作指针操作表达式的操作数。
const size_t arr_sz = 5;
int int_arr[arr_sz] = {0, 1, 2, 3, 4};
// pbegin points to first element, pend points just after the last
for(int *pbegin = int_arr, *pend = int_arr + arr_sz;pbegin != pend; ++pbegin)
   cout << *pbegin << " "; // print current element
cout << endl; 

指针与const限定符:将指针与const限定符组合在一起,我们可以得到许多有意思的东西。包括指向const对象的指针,const指针,指向const对象的const指针以及在typedef中使用指针所带来的问题。

1)指向const对象的指针,C++语言强制要求指向const对象的指针也必须具有const特性。

const double *cptr;// cptr may point a double that is const
这里的cptr是指向一个double类型const对象的指针,const限定了cptr指针所指向对象类型,而并非cptr本身,也就是说cptr本身并不是const。在定义是不需要对它进行初始化,如果需要的话,允许给cptr重新赋值,使其指向另一个const对象,但不能通过cptr修改所指对象的值。当然我们也允许把非const对象的地址赋给指向const对象的指针,而且也不允许通过该指针来修改这个非const对象,尽管我们可以通过其他方法来修改其这个非const对象的值。因此我们通常把指向const对象的指针理解为“自以为指向const对象的指针”

2)const指针,指针本身的值不能修改,与任何const对象一样,const指针也必须在定义时初始化。

int errNumb = 0;
int *const curErr = &errNumb; // curErr is a constant pointer
我们可以从右向左把上述语句读作“curErr 是指向int型对象的const指针”。指针本身是const的事实没有说明能否用该指针修改所指向的对象。指针所指的对象的值能否修改完全取决于该对象的类型。

3)指向const对象的const指针,既不能修改该指针所指向对象的值,也不能修改该指针的指向。

const double pi = 3.14159;
const double *const pi_ptr = &pi; // pi_ptr is const and points to a const object
4)指针和typedef,在typedef中使用指针往往会带来意外的结果。
typedef string *pstring;
const pstring cstr1; // cstr1 is a const pointer to string
string *const cstr2;//  equivalent to const pstring ctsr1
pstring const cstr3;// they're all const pointers to string
3.C风格的字符串(C-style character string)

前面我们使用字符串字面值的时候里了解到字符串字面值的类型是字符常量数组,现在我们更加可以明确的认识到,字符串字面值的类型就是const char 类型的数组。C++ 从C语言继承下来的一种通用结构是C风格字符串(C-style charcter string),而字符串字面值就是该类型的实例。实际上,C风格字符串既不能确切归结为C语言的类型,也不能归结为C++语言的类型,而是以空字符null结束的字符数组。

char ca1[] = {'C', '+', '+'}; //no null, not C-style string
char ca2[] = {'C', '+', '+', '\0'}; // explicit null
char ca3[] = "C++"; // null terminator added automatically 
const char *cp = "C++";  // null terminator added automatically 
char *cp1 = ca1; // no
char *cp2 = ca2;// yes
C风格字符串的使用:C++语言通过(const)char*类型的指针来操纵C风格字符串。一般来说我们使用指针的算术操作来遍历C风格字符串,每次对指针进行测试并递增1,直到到达结束符null为止。
char ca[] = "Hello, C++!";
const char *p = ca;
while(*p){
    cout << *p;
    ++p;
}
C语言标准库提供了一系列处理C风格字符串的库函数,要使用这些标准库函数,必须包含响应的C头文件:
#include <cstring>
传递给这些标准库函数例程的指针必须具有非零值,并且指向以null结束的字符数组中的第一个元素,其中一些标准库函数会修改传递给他的字符串,这些函数将假定它们所修改的字符串具有足够大的空间接受本函数新生成的字符,程序员必须确保目标字符串足够大。

操纵C风格字符串的标准库函数
strlen(s)
返回s的长度,不包括字符串结束字符null
strcmp(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

使用strn函数处理C风格字符串,比直接使用str函数要安全。

const char *cp1 = "A string example";
const char *cp2 =  "A different string";
char largeStr[16+18+2]; // to hold cp1 a space and cp2
strncpy(largeStr, cp1, 17); //size to copy includes the null
strncat(largeStr, " ", 2); // pedantic, but a good habit
strncat(largeStr, cp2, 19); //adds at most 18 character, plus a null
使用标准库函数strncar和strncpy的诀窍在于可以适当地控制复制字符的个数。特别实在复制和串连字符串时,一定要时刻记住算上结束符null。在定义字符串时切记要预留存放null字符的空间,因为每次调用标准库函数后必须以此结束字符串largeStr。

尽量使用标准库类型string:如果使用标准库类型string,则不存在上述问题。

string largeStr = cp1; // initiaize largeStr as a copy of cp1
largeStr += " "; // add space at end of largeStr
largeStr += cp2; // concatenate cp2 onto end of largeStr 

对于大部分的应用而言,使用标准库类型string,除了增强安全性外,效率也提高了,因此应该尽量避免使用C风格字符串。

创建动态数组:数组类型的变量有三个重要的限制,数组长度固定不变,在编译时必须知道其长度,数组只能在定义它的语句块内存在。实际的程序往往需要在运行时动态的分配数组。虽然数组长度是固定的,但是动态分配的数组不必在编译时知道其长度,可以在运行时才确定数组的长度。与数组变量不同,动态分配的数组将一直存在,直到程序显示释放它为止。

每一个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此空间成为程序的自由存储区(free store)或堆(heap)。C语言程序使用一对标准库函数malloc和free在自由存储区中分配存储空间,而C++语言则使用new和delete表达式实现同样的功能。

int *pia1 = new int [10]; //array of 10 uninitialized ints
int *pia2 = new int [10](); //array of 10 initialized ints, values is 0
string *psa = new string[10]; // array of 10 empty strings
const int *pci_bad = new const int[100]; //error:unintialized const array
const int *pci_ok = new const int[100](); //ok:value-initialized const array
...
size_t n = get_size(); // get_size returns number of elements needed
int *p = new int[n];
...
delete [] pia1 ;//release pia1 array,free the memory
数组变量通过指定类型、数组名和维数来定义。而动态分配数组时,只需要指定类型和数组的长度,不必为数组对象命名,new表达式返回指向新分配数组的第一个元素的指针。动态分配数组时,如果数组元素具有类类型,将使用该类类型的默认构造函数实现初始化;如果数组元素是内置类型,则无初始化,但我们也可以使用跟在数组长度后面的一对圆括号,对数组元素做值初始化。C++语言为指针提供delete [] 表达式释放指针所指向的数组空间。其中关键字delete和指针之间的方括号对是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而非单个对象。

动态数组的使用:通常是因为在编译时无法知道数组的维数,所以才需要动态的创建数组。例如,在程序运行过程中,常常使用char*指针指向多个C风格的字符串,于是必须根据每个字符串的长度实时地动态分配存储空间。采用这种技术要比建立固定大小的数组安全。如果程序员能够准确计算出运行时需要的数组长度,就不必担心因数组变量具有固定的长度而造成的溢出问题。

#include <iostream>
#include <cstring>
using namespace std;
int main(){
    const char *noerror = "success";
    // ...
    const char *err189 = "Error: a function declaration must"
                         "specify a function return type!";
    const char *errorTxt;
    int errorFound;
    cout << "errorFound: " <<errorFound << endl;
    cin >> errorFound;
    if(errorFound)
        errorTxt = err189;
    else
        errorTxt = noerror;
    size_t dimension = strlen(errorTxt)+1;
    char *errMsg = new char[dimension];
    cout << "errMsg: " << errMsg <<endl;
    strncpy(errMsg, errorTxt, dimension);
    cout << "errMsg: " << errMsg <<endl;
    delete [] errMsg;
    cout << "errMsg: " << errMsg <<endl;
    return 0;
}

新旧代码的兼容:许多C++ 程序在有标准类之前就已经存在了,因此既没有使用标准库类型string也没有使用vector。而且许多C++程序为了兼容现存的C程序,也不能使用C++标准库。因此现代的C++程序必须兼容使用数组和C风格字符串代码,标准库提供了使兼容性更容易的管理手段。

1)混合使用标准库类string和C风格字符串

正如前面显示的,可用字符串字面值初始化string类对象。通常,由于C风格字符串与字符串字面值具有相同的数据类型,而且都是以空字符null结束,因此可以把C风格字符串用在任何可以使用字符串字面值的地方。比如可以使用C风格字符串对string对象进行初始化或赋值;string类型的加法操作需要两个操作数,可以使用C风格字符串作为其中一个操作数,也允许将C风格字符串用作符合赋值操作的右操作数。反之则不成立,在要求C风格字符串的地方不可直接使用标准库string类型对象,例如无法使用string对象初始化字符指针。但是string类提供了一个名c_str的成员函数可以返回C风格的字符串。

string st2("Hello, world!");
//char *str = st2;
//char *str = st2.c_str();
const char *str = st2.c_str();
cout << str <<endl;
//st2 = st2 + "--By Wayne";
cout << str <<endl;

2)使用数组初始化vector对象

不能用一个数组直接初始化另一个数组,程序员只能创建新数组,然后显示的把源数组的元素逐个复制给新数组。这反映C++允许使用数组初始化化vector对象。使用数组初始化对象,必须指出用于初始化的第一个元素以及数组最后一个元素的下一个位置的地址。

const size_t arr_size = 6;
int int_arr[arr_size] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(int_arr, int_arr+arr_size);
for(vector<int>::iterator iter = ivec.begin();iter != ivec.end();++iter)
    cout << *iter << " ";
cout << endl;

4.多维数组

严格的来说,C++中没有多维数组,通常所指的多维数组其实就是数组的数组。

int ia[3][4]; //array of size 3, each element is an array of ints of size 4
多维数组的初始化:和处理一维数组一样,程序员可以使用花括号括起来的初始化列表来初始化多维数组元素。对于多维数组的每一行,可以再用花括号指定其元素的初始化式。
int ia1[3][4] = { {0, 1, 2, 3},{4, 5, 6, 7},{8, 9, 10 , 11}};
//equivalent initialization without the optional nested braces for each now
int ia2[3][4] = { 0, 1, 2, 3, 4, 5, 6 7, 8, 9, 10, 11};
//explicitly initialize only element 0 in each row
int ia3[3][4] = {{0}, {4}, {8}}
多维数组的下标引用:为了多维数组进行索引,每一维都需要一个下标。当需要访问数组中的特定元素时,必须提供其行下标和列下标。行下标指出需要哪个内部数组,列下标则选取该内部数组的指定元素。

指针和多维数组:与普通数组一样,使用多维数组名时,实际上将自动转换为指向该数组第一个内层数组的指针。定义指向数组的指针与如何定义数组本身类似,首先声明元素类型,后接(数组)变量的名字和维数。窍门在于(数组)变量的名字其实就是指针,因此需要在标识符前加上*。

int ia[3][4];
int (*ip)[4] = ia; // ip points to an array of 4 ints
ip = &ia[2]; // ia[2] is an array of 4 ints
使用typedef类型定义可使指向多维数组元素的指针更容易读、写和理解:
typedef int int _array[4];
int_array *ip = ia;

可以使用typedef 类型输出ia的元素:

for(int_array *p = ia; p != ia +3; ++p)
    for(int *q = *p; q != *p+4; ++q)
        cout << *q << endl;

(完)


56.8K

发表评论:

© 云中布衣 2015 | Driven by EMLOG  | SiteMap | RunTime: 7.50ms&RSS  |   | 回到顶部

文章数量【230】 评论数量【156】 稳定运行【1031天】

Visitor IP Address【54.80.82.9】