数组是具有一定顺序关系的若干对象的集合体,组成数组的对象称为该数组的元素。数组可以由除void以外的任何类型构成。
数组名是一个指向数组第一个元素的指针常量,其自身的值不能更改。数组定义时,不能用另外一个已定义的数组的数组名进行初始化。
如:
Int a[3]= {0};
Int b[3]= a; //error 数组定义时,不能用另外一个已定义的数组进行初始化。
a++; //error 自身的值不能更改
声明一个数组类型,应该包括以下几个方面:
确定数组元素的类型
确定数组的名称
确定数组的结构(包括数组的维数,每一维的大小等)
数组的使用:
数组元素的下标表达式可以是任意合法的算术表达式,其结果必须为整数。
数组元素的下标值不得超过声明时所确定的上下界,否则运行时将发生数组越界错误。
提示:如果发生了数组越界,运行时有时会得到提示,但有时却得不到任何提示,不可预期的结果会悄悄的发生。
数组的存储
数组元素在内存中是顺序、连续存储的。
数组的初始化
数组的初始化就是在声明数组时给部分或全部元素赋初值。
细节:当指定的初值个数小于数组大小时,剩下的数组元素会被赋予0值。若定义数组时没有指定任何一个元素的初值,对于静态生存期的数组,每个数组仍然会被赋予0值;但对于动态生存期的数组,每个元素的初值都是不确定的。但是声明为常量的非静态数组所有元素必须全部赋初值
constfloat a[3] = {1.0f}; //error 必须全部赋初值
staticconst float a[3] = {1.0f}; //OK a[1]、a[2]会被初始化为0
指针
指针概述
指针变量是用来存放内存单元地址的。通过变量名访问一个变量是直接的,而通过指针访问一个变量是间接的。
当在程序中声明一个变量时声明了以下信息:
变量需要的内存空间
限定了对变量可以进行的运算及其运算规则。
void指针
void类型指针,可以存储任何类型的对象指针,就是说任何类型的指针都可以赋值给void类型的指针变量。经过使用类型显示转换,通过void类型的指针便可以访问任何类型的数据。可以声明void类型的指针,但不能声明void类型的变量。
如:
int a =8;
//voidb; //error不能声明void类型的变量
void *p; //可以声明void类型的指针
p =&a; // void类型指针,可以存储任何类型的对象指针
cout<< *static_cast<int *>(p) << endl; //输出8
提示:void指针一般只在指针所指向的数据类型不确定时使用。
指针运算
指针的算术运算是和数组的使用相联系的,因为只有在使用数组时,才会得到连续分布的可操作内存空间。不同类型的指针之间或指针与非0整数之间的关系运算是毫无意义的。但是指针变量可以和整数0进行比较。0专用于表示空指针,也就是一个不指向任何有效地址的指针。赋给指针变量的值必须是地址常量(如数组名)或地址变量或0,不能是非0整数。
细节:空指针也可以用NULL来表示。NULL是一个在很多头文件中都有定义的宏,被定义为0.
习惯:如果不便于用一个有效地址给一个指针变量赋初值,那么应该用0作为它的初值,从而避免指向不确定地址的指针出现。
指针数组
由于指针数组的每一个元素都是指针,必须先赋值后引用,因此,声明数组之后,对数组元素赋初值是必不可少的。
指针作为函数参数
指针作为函数参数的作用:
①使实参与形参的指针指向共同的内存空间,以便在函数中修改实参的值。
②减少函数调用时数据传递的开销。
③通过指向函数的指针传递函数代码的首地址。
习惯:如果函数体中不需要通过指针改变指针所指向的内容,应在参数表中将其声明为指向常量的指针,这样使得常对象被取地址后也可以作为该函数的参数。
指针型函数
顾名思义,指针型函数就是返回值类型为指针类型的函数。使用指针型函数的最主要目的就是要在函数结束时把大量的数据从被调函数返回到主调函数中。而通过非指针型函数调用结束后,只能返回一个变量或对象。
智能指针
四个智能指针(auto_ptr、unique_ptr、shared_ptr和weak_ptr)都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。
auto_ptr类的部分定义
template<classT>
class auto_ptr
{
public:
explicit auto_ptr(T* p = 0)throw();
……
};
auto_ptr和unique_ptr
赋值操作转让所有权。auto_ptr和unique_ptr实现机制相同,但unique_ptr更严格。
auto_ptr对象赋值,转让了所有权之后就不能使用该对象了。
auto_ptr<string> ps(newstring("Hello"));
auto_ptr<string> ps1 = ps;//ok ps将所有权转让给ps1
//cout<< *ps << endl;//error
cout<< *ps1 << endl;//ok
unique_ptr对象赋值,右值必须是临时对象。
unique_ptr<string> ps(newstring("Hello"));
//unique_ptr<string> ps1 = ps;//error
unique_ptr<string> ps2 =unique_ptr<string>(newstring("Hello"));//ok 通过临时对象赋值
shared_ptr
引用计数。赋值时,计数将加1,而指针过期时,计数将减1.仅当最后一个指针过期时,才调用delete。
注意:auto_ptr使用时有安全隐患,所以已经摒弃。正因为如此,禁止在容器对象中使用auto_ptr。此外,unique_ptr可以用于数组,支持new[]和delete[],而auto_ptr和shared_ptr不行。
unique_ptr<int[]> np(newint[7]);
函数指针
声明一个函数指针时,也需要说明函数的返回值、形式参数列表。
函数名在表示函数代码起始地址的同时,也包括函数的返回值类型和参数的个数、类型、排列次序等信息。因此在通过函数名调用函数时,编译系统能够自动检查实参与形参是否相符,用函数的返回值参与其他运算时,能自动进行类型一致性检查。
提示:用typedef可以很方便地为复杂类型起别名。
函数指针在使用之前也要进行赋值,使指针指向一个已存在的函数代码的起始地址。
函数指针名 = 函数名;
等号右边的函数名所指向的必须一个已经声明过的、和函数指针具有相同返回值类型和相同形参表的函数。在赋值之后,就可以通过函数指针名来直接引用这个指针指向的函数。
对象指针
对象指针在使用之前,也一定要先进行初始化,让它指向一个已经声明过的对象,然后再使用。通过对象指针,可以访问到对象的公有成员。
this指针
this指针是一个隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数和析构函数),它用于指向正在被成员函数操作的对象。
细节:this指针实际上是类成员函数的一个隐含参数。对于常成员函数来说,这个隐含的参数是常指针类型。
this指针明确的指出了成员函数当前所操作的数据所属的对象。this是一个指针常量,对于常成员函数,this同时又是一个指向常量的指针常量。在成员函数中,可以使用*this来标识正在调用该函数的对象。
例如:
class A
{
public:
A(int x = 0):x(x)
{
cout << "构造函数"<< endl;
}
A& assign(const A &ref)
{
this->x = ref.x;
return *this;//返回当前对象的引用,以便实现链式操作
}
int getX()
{
return x;
}
private:
int x;
};
A a;
A b(6);
A c(8);
a.assign(b).assign(c);//链式操作 先用b的值给a赋值,再用c的值给a赋值。a的x值为8
提示:当局部作用域中声明了与类同名的标识符时,对该标识符的直接引用代表的是局部作用域中所声明的标识符,这时为了访问该类成员,可以通过this指针。
指向类的非静态成员的指针
声明指针语句的形式:
类型说明符 类名::*指针名; //声明指向数据成员的指针
类型说明符 (类名::*指针名)(参数表) [const]; //声明指向函数成员的指针
对成员指针赋值的语句形式:
指针名 = &类名::数据成员名; // 给指向数据成员的指针赋值
指针名 = [&]类名::函数成员名; // 给指向函数成员的指针赋值
注意:对类成员取地址时,也要遵守访问权限的约定,也就是说,在一个类的作用域之外不能对它的私有成员取地址。
通过指针访问成员的语句形式:
对象名.*类数据成员指针名;
对象指针名->*类数据成员指针名;
(对象名.*类函数成员指针名)(参数表);
(对象指针名->*类函数成员指针名)(参数表);
这里通过函数指针访问成员的语句同普通函数指针有点不同,访问时不要忘了“*”,否则编译出错。
注意:普通成员函数与常成员函数具有不同的类型,因此能够被常成员函数赋值的指针,需要声明时明确写出const关键字。
在对象创建之前可以定义指向类的非静态数据成员的指针,并用类的非静态数据成员的地址对其进行初始化。由于还没有创建对象,所以在指针所指向的内存空间中保存的是非静态数据成员的偏移量。当对象通过非静态数据成员的指针访问数据成员时,通过对象的地址加偏移量来找到相应的数据成员。
指向类的静态成员的指针
指针的语句形式:
类型说明符 *指针名 = &类名::静态数据成员名;
类型说明符 (*指针名)(参数表) =[&]类名::静态函数成员名;
*指针名; //访问静态数据成员
指针名(参数表); //访问静态函数成员名
指向类的静态成员的指针的定义及使用,同普通的指针变量和函数指针变量完全一样。
动态内存分配
Int *pi= new int(8);//初值设为8
细节:对于基本数据类型,如果不希望在分配内存后设定初值,可以把括号省去。
Int *pi= new int;//初值为随机值
如果保留括号,但括号中不写任何数值,则表示用0对该对象初始化。
Int *pi= new int();//初值为0
细节:对于一个类T,如果用户定义了无参构造函数,则“new T”和“new T()”效果一样。如果用户没有定义构造函数,则“new T”调用系统生成的隐含默认构造函数,其效果和前面的一样,但“new T()”除了执行默认构造函数外,还会为基本数据类型和指针类型的成员用0赋初值,而且这一过程是递归的。
动态创建对象数组
类型名 *指针名 = new 类型名[数组长度];//动态创建一维数组
其中数组长度指出了数组元素个数,它可以是任何能够得到正整数值的表达式(包括整型变量),只有动态创建数组时,数组第一维的长度才可以是整型变量。
数组长度也可以是0(此时不分配空间),当程序员没有编写析构函数时,下列程序能正常运行,new和delete不调用构造函数和析构函数;当程序员编写了析构函数时(即使函数体为空也不行),下列程序不能正常运行,new和delete调用构造函数和析构函数,但在调用析构函数时出错(VC 6.0中。程序终止;visual studio 2013中,进程阻塞)。
如:
T *p =new T[0];
deletep;
细节:用new动态创建一维数组时,在方括号后仍然可以加小括号“()”(加小括号的效果是用0初始化数组所有元素),但小括号内不能带任何参数。
类型名 *指针名 = new 类型名[第1维长度] [第2维长度]……; //动态创建多维数组
其中数组第1维长度可以是任何结果为正整数的表达式(可以是整型变量),而其他各维数组长度必须是结果为正整数的常量表达式。
一个程序一般可以以两种模式编译,调试(debug)模式和发行(release)模式,assert只在调试模式下生效,而在发行模式下不执行任何操作,这样兼顾了调试模式的调试需求和发行模式的效率需求。
提示:由于assert只在调试模式下生效,一般用assert只是检查程序本身的逻辑错误,而用户的不当输入造成的错误,则应当用其他方式加以处理。
delete NULL;//OK delete后面可以是NULL,实际不释放空间
用vector创建数组对象
vector不是一个类,而是一个类模板。
vector<元素类型> 数组对象名(数组长度);
尖括号中的类型名表示数组元素类型。数组长度是一个表达式,表达式中可以包含变量。
细节:与普通数组不同的是,用vector定义的数组对象的所有元素都会被初始化。如果数组元素的类型为基本数据类型,则所有元素都会被初始化为0;如果数组元素为类类型,则会调用类的默认构造函数初始化。因此如果以此形式定义的vector动态数组,需要保证作为数组元素的类具有默认构造函数。另外,初值也可以自己指定,但所有元素只能指定相同初值。
Vector<元素类型> 数组对象名(数组长度, 元素初值);
深复制与浅复制
当类的构造函数需要动态分配空间,而析构函数要释放空间,当通过默认的复制构造函数创建临时对象时,没有分配空间,只是使两个对象指针指向相同的内存空间。当两个对象析构时,同一内存空间被释放两次,程序运行异常,出现错误。此时,必须重写复制构造函数,并在复制构造函数中给临时对象分配空间,使两个对象指针指向不同的内存空间,即深拷贝。
这样,两个对象就是独立的,互不影响。
字符串
string类
严格的说,string并非一个独立的类,而是类模板basic_string的一个特化实例。
提示:由于string类具有接收constchar*类型的构造函数,因此字符串常量和用字符数组表示的字符串变量都可以隐含的转换为string对象。
例如:
stringstr = “Hello world!”;
直接使用cin的”>>”操作符从键盘输入字符串,以这种方式输入时,空格会被作为输入的分隔符。
当输入的字符串中有空格时,可以使用getline,直到行末为止。
stringstr;
getline(cin,str);//输入时,以回车作为分隔符
当然,也可以自己定义分隔符,在getline的第三个参数位置设置自定义分隔符。
getline(cin,str, ‘,’);//输入时,以逗号作为分隔符
指针与引用
引用本身(而非被引用的对象)的地址是不可以获得的,引用一经定义后,对它的全部操作,全是针对被引用对象的,而引用本身所占用的空间则被完全隐藏起来了。
注意:只有常引用,而没有引用常量,也就是说,不能用T & const作为引用类型。这是因为引用只能在初始化时指定它所引用的对象,其后则不能再更改,这使得引用本身(而非被引用的对象)已经具有常量性质了。
下列情况,引用无法替代指针:
①如果一个指针所指向的对象频繁更改
②使用函数指针,由于没有函数引用,函数指针无法被引用替代。
③用new动态创建的对象或数组,需要用指针来存储它的地址。
④以数组形式传递大批量数据时,需要用指针类型接收参数。
细节:对于后面两种情况,指针并非完全不能用引用代替。
例如:
T &s= *(new T());//对对象new返回的指针进行解引用后初始化引用变量
delete&s;// 取被引用对象的地址,用delete将其空间释放
指针的安全性隐患及其应对方案
地址安全性
尽量不直接通过指针来使用数组,而是使用封装的数组(vector)。
类型安全性
static_cast<>
dynamic_cast<>
reinterpret_cast<>
const_cast<>
堆对象的管理
通常使用的局部变量,在运行栈上分配空间,空间分配和释放的过程是由编译器生成的代码控制的,一个函数返回后相应的空间会自行释放;而静态生存期变量,其空间的分配是由连接器来完成的,它们占据的空间大小始终是固定的,在运行的过程中无须释放。然而,用new在程序运行时动态创建的堆对象,则必须由程序用delete显示删除。