此份笔记建议在完整阅读郑莉老师、董渊老师、何江舟老师所编写的《C++语言程序设计(第4版)》后食用,风味更佳!
最后,由于本人水平有限,笔记中仍存在错误但还没有被检查出来的地方,欢迎大家批评与指正。
第6章 数组、指针与字符串
6.1 数组
数组是具有一定顺序关系的若干对象的集合体,组成数组的对象称为该数组的元素。
6.1.1 数组的声明与使用
1.声明一个数组类型,应包括以下几个方面:
- 确定数组的名称
- 确定数组元素的类型
- 确定数组的结构(包括数组的维数,每一维的大小等)
2.数组声明的一般形式
数据类型 标识符[常量表达式1][常量表达式2]...;
- 声明数组时,每一个常量表达式标识该维的下标个数而不是下标上界;下标上界应该是下标个数减去1。
3.实例
int b[10]; //声明一个名为b的int型数组,共可以存放10个int型数据
int a[5][3]; //声明一个名为a的int型的二维数组,5行3列
4.数组的使用
数组名[下标表达式1][下标表达式2]...
- 数组中的每一个元素都相当于一个相应类型的变量,凡是允许使用该类型变量的地方都可以使用数组元素。
- 数组元素的下标表达式可以是任意合法的算术表达式,其结果必须为整数。
- 数组元素的下标值不得超过声明时所确定的上下界,否则运行时将出现数组越界错误。
6.1.2 数组的存储与初始化
数组元素在内存中是顺序、连续存储的。
1.一维数组的存储:简单按照下标的顺序,连续存储
int arr[5];
2.二维数组的存储:按行存放,即行优先存储
int m[2][3];
- 理解方式:将m数组看作一个一维数组,大小为2;其中每一个元素都是一个一维数组,大小为3。
3.多维数组的存储
int m[2][3][4];
4.数组的初始化
数组的初始化就是在声明数组时给部分或全部元素赋初值。
对于基本类型的数组,初始化过程就是给数组元素赋值;对于对象数组,每个元素都是某个类的一个对象,初始化就是调用该对象的构造函数。
(1)声明数组,同时给出所有元素的初值
int a[3]={1,1,1};
(2)声明数组,同时列出全部元素的初值,可以不用说明数据元素个数
int a[]={1,1,1};
(3)声明数组,只对数组中的部分元素进行初始化
float fa[5]={1.0,2.0,3.0};
- 给出的值,语法默认按数组元素下标从下到大顺序赋值;也就是初始化只能针对所有元素或从起始地址开始的前若干元素,而不能间隔赋值。
- 这样的赋值方式,数组元素的个数一定要明确指出。
- 当指定的初值个数小于数组大小时,剩下的数组元素会被赋予0值。
(4)声明数组,仅指出数组大小,没有指定任何一个元素的初值
- 对于静态生存期的数组,每个元素仍然会被赋予 0 值;
- 但对于动态生存期的数组,每个元素的初值都是不确定的。
(5)多维数组的初始化
int a[2][3]={1,0,0,0,1,0};
//等价于
int a[][3]={1,0,0,0,1,0} //如果给出全部元素的初值,第一维的下标个数可以不用显式说明
//等价于
ine a[2][3]={{1,0,0},{0,1,0}};
6.1.3 数组作为函数参数
数组元素和数组名都可以作为函数的参数以实现函数间数据的传递和共享。
1.数组元素作为调用函数时的实参
- 数组元素作为调用函数时的实参,这与使用该类型的一个变量(或对象)作实参是完全相同的。
2.数组名作函数的参数
-
如果使用数组名作函数的参数,则实参和形参都应该是数组名,且类型要相同。
-
和普通变扯作参数不同,使用数组名传递数据时,传递的是地址。
-
形参数组和实参数组的首地址重合,后面的元素按照各自在内存中的存储顺序进行对应,对应元素使用相同的数据存储地址,因此实参数组的元素个数不应该少千形参数组的元素个数。
-
如果在被调函数中对形参数组元素值进行改变,主调函数中实参数组的相应元素值也会改变,这是值得特别注意的一点。
-
可以在形参前加上 const 修饰,保证对数组只读不写。
-
#include<iostream> using namespace std; void fun(const int *p) { int i; for(i=0;i<5;i++) { cout << *(p+i) << endl; } *p = 1; //编译报错,提示“表达式必须是可修改的左值” } int main() { a[5]={1,6,5,3,9}; fun(a); return 0; }
-
6.1.4 对象数组
数组的元素不仅可以是基本数据类型,也可以是自定义类型。
1.声明一个一维对象数组的语法
类名 数组名[常量表达式];
2.一维对象数组元素访问它的公有成员方式
数组名[下标表达式].成员名
3.对象数组的初始化
对象数组的初始化过程,实际上就是调用构造函数对每一个元素对象进行初始化的过程。
- 如果在声明数组时给每一个数组元素指定初始值,在数组初始化过程中就会调用与形参类型相匹配的构造函数。
Location a[2]={Location(1,2),Location(3,4)};
- 如果没有指定数组元素的初始值,就会调用默认构造函数。
Location a[2]={Location(1,2)};
- 如果需要建立某个类的对象数组,在设计类的构造函数时就要充分考虑到数组元素初始化时的需要;当各元素对象的初值要求为相同的值时,应该在类中定义默认构造函数;当各元素对象的初值要求为不同的值时,需要定义带形参(无默认值)的构造函数。
6.2 指针
6.2.1 内存空间的访问方式
1.变量什么时候被分配内存空间
- 具有静态生存期的变量在程序开始运行之前就已经被分配了内存空间。
- 具有动态生存期的变量,是在程序运行时遇到变量声明语句时被分配内存空间的。
2.变量名的真实含义——内存空间的别名
在变量获得内存空间的同时,变量名也就成了相应内存空间的名称,在变量的整个生存期内都可以用这个名字访问该内存空间,表现在程序语句中就是通过变量名存取变量内容。
6.2.2 指针变量的声明
指针也是一种数据类型,具有指针类型的变星称为指针变量。
指针变量是用于存放内存单元地址的。
通过变量名访问一个变量是直接的,而通过指针访问一个变量是间接的。
1.声明指针的语法
数据类型 *标识符;
- *表示这里声明的是一个指针类型的变量
- 数据类型可以是任意类型,指的是指针所指向的对象(包括变最和类的对象)的类型,这说明了指针所指的内存单元可以用于存放什么类型的数据,称之为指针的类型。
- 为什么声明指针变量时要指出它所指的对象是什么类型?
- 声明变量需要的内存空间
- 限定了对变量可以进行的运算及其操作
- 所以,声明指针时需要明确指出它用于存放什么类型数据的地址。
- 为什么声明指针变量时要指出它所指的对象是什么类型?
6.2.3 与地址相关的运算“*”和“&”
同C语言,不再详述。
6.2.4 指针的赋值
定义了一个指针,只是得到了一个用于存储地址的指针变量,但是变量中并没有确定的值,其中的地址值是一个不确定的数。也就是说,不能确定这时候的指针变量中存放的是哪个内存单元的地址。这时候指针所指的内存单元中有可能存放着重要数据或程序代码,如果盲目去访问,可能会破坏数据或造成系统的故障。
所以,定义指针之后必须先赋值,然后才可以引用。
1.指针赋值的两种形式
(1)在定义指针的同时进行初始化赋值。
存储类型 数据类型 *指针名=初始地址;
(2)在定义之后,单独使用赋值语句。
存储类型 数据类型 *指针名;
指针名=地址;
- 如果使用对象地址作为指针的初值,或在赋值语句中将对象地址赋给指针变最,该对象必须在赋值之前就声明过,而且这个对象的类型应该和指针类型一致。
- 也可以使用一个已经赋值的指针去初始化另一个指针;也就是说,可以使多个指针指向同一个变量。
2.如何取地赋值给指针变量——&取地运算符
- 对于基本类型的变量、数组元素、结构成员、类的对象,可以使用取地址运算符&来获得它们的地址。
int a=10;
int *p=&a;
3.关于数组名
- 一个数组,可以用它的名称来直接表示它的起始地址。
- 数组名称实际上就是一个不能被赋值的指针,即指针常量。
int a[10];
int *p=a;
4.关于指针的类型
(1)指向常量的指针
int a;
const int *ptr=&a;
const char *pc="abcd";
- 不能通过指针改变所指对象的值
- 可以改变指针的值,即可以指向其他对象
(2)常指针
int a;
int* const ptr=&a;
char* const pc="abcd";
- 不可以改变指针的值,即不允许改变指针所指对象
- 允许改变指针所指对象的值
(3)指向常量的常指针
const char* const pc="abcd";
- 不能通过指针改变所指对象的值
- 不可以改变指针的值,即不允许改变指针所指对象
(3)void类型指针
- 一 般情况下 ,指针的值只能赋给相同类型的指针。
- void 类型指针,可以存储任何类型的对象地址,就是说任何类型的指针都可以赋值给 void 类型的指针变量。
- 经过使用类型显式转换,通过 void 类型的指针便可以访问任何类型的数据。
#include<iostream>
using namespace std;
int main()
{
void *ptr;
int i=5;
ptr=&i;
int *pint=static_cast<int*>(ptr);
cout << "*pint=" << *pint << endl;
return 0;
}
- void 指针一般只在指针所指向的数据类型不确定时使用。
6.2.5 指针运算
1.算术运算
- 对指针进行算术运算时,一定要确保运算结果所指向的地址是程序中分配使用的地址。
- 指针算术运算的不慎使用会导致指针指向无法预期的地址,从而造成不确定的结果,因此指针的算术运算一定要慎用。
2.关系运算
- 指针变量的关系运算指的是指向相同类型数据的指针之间进行的关系运算。
- 如果两个相同类型的指针相等,就表示这两个指针是指向同一个地址。
- 不同类型的指针之间或指针与非 0 整数之间的关系运算是毫无意义的。但是指针变量可以和整数0进行比较。
- 0专用于表示空指针,也就是一个不指向任何有效地址的指针。
3.赋值运算
- 赋给指针变最的值必须是地址常量(如数组名)或地址变量 ,不能是非 0 的整数 。
- 但可以给一个指针变量赋值为 0,这时表示该指针是一个空指针,不指向任何地址。
int *p;
p=0; //将p设置为空指针,不指向任何地址
int *ptr=NULL; //空指针也可以用NULL来表示
4.思考:为什么要将一个指针设置为空指针?
这是因为有时在声明一个指针时并没有一个确定的地址值可以赋给它,当程序运行到某个时刻才会将某个地址赋给该指针。这样,从指针变量诞生起到它具有确定的值之前这一段时间、其中的值是不确定的。如果误用这个不确定的值作为地址去访问内存单元,将会造成不可预见的错误。因此在这种情况下便首先将指针设置为空。
6.2.6 用指针处理数组元素
指针加减运算的特点使得指针特别适合于处理存储在一段连续内存空间中的同类数据。
而数组恰好是具有一定顺序关系的若干同类型变最的集合体,数组元素的存储在物理上也是连续的,数组名就是数组存储的首地址。这样,便可以使用指针来对数组及其元素进行方便而快速的操作。
6.2.7 指针数组
1.什么是指针数组
- 如果一个数组的每个元素都是指针变量,这个数组就是指针数组。
- 指针数组的每个 元素都必须是同一类型的指针。
2.声明一维指针数组的语法
数据类型 *数组名[下标表达式];
3.思考:一个二维的数组n行m列,我们可以把它看作是n行的一维数组组成,再收缩一下,相当于是一个一维的指针数组,这个数组里的元素就是每一行的一维数组的数组名。
实例研究:
#include<iostream>
using namespace std;
int main()
{
int line1[3]={11,12,13};
int line2[3]={14,15,16};
int line3[3]={17,18,19};
int *pLine[3]={line1,line2,iline3};
int array[3][3]={{11,12,13},{14,15,16},{17,18,19}};
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
cout << pLine[i][j] << " ";
}
cout << endl;
}
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
cout << *(*(pLine+i)+j) << ""
}
cout << endl;
}
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
cout << array[i][j] << " ";
}
cout << endl;
}
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
cout << *(*(array+i)+j) << ""
}
cout << endl;
}
return 0;
}
6.2.8 用指针作为函数参数
- 如果函数体中不需要通过指针改变指针所指向对象的内容,应在参数表中将其声明为指向常量的指针,这样使 得常对象被 取地址后也可作为该函数的参数。
6.2.9 指针型函数
- 当一个函数的返回值是指针类型时,这个函数就是指针型函数。
- 使用指针型函数的最主要目的就是要在函数结束时把大量的数据从被调函数返回到主调函数中。而通常非指针型函数调用结束后,只能返回一个变量或者对象。
6.2.10 指向函数的指针
每一个函数都有函数名,实际上这个函数名就表示函数的代码在内存中的起始地址。
- 函数指针就是专门用来存放函数代码首地址的变量。
- 函数名在表示函数代码起始地址的同时,也包括函数的返回值类型和参数的个数、类型、排列次序等信息。
1.声明一个函数指针的语法
数据类型 (*函数指针名)(参数表)
- 数据类型说明函数指针所指函数的返回值类型;第一个圆括号中的内容指明一个函数指针的名称;形参表则列出了该指针所指函数的形参类型和个数。
2.对函数指针赋值的语法
函数指针名 = 函数名;
- 函数指针的赋值,使指针指向一个已经存在的函数代码的起始地址。
- 等号右边的函数名所指出的必须是一个已经声明过的、和函数指针具有相同返回类型和相同形参表的函数。
3.实例
#include<iostream>
using namespace std;
void printStuff(float){
cout << "1" << endl;
}
void printMessage(float data){
cout << "2" << endl;
}
void printFloat(float data){
cout << "3" << endl;
}
int main()
{
void (*functionPtr)(float);
printStuff(12.3);
printMessage(10.6);
printFloat(3.14);
functionPtr=printStuff;
functionPtr(15.6);
functionPtr=printMessage;
functionPtr(11.8);
functionPtr=printFloat;
functionPtr(3.6);
rerurn;
}
4.思考:如果要让多个不同的函数指针指向同一个函数,那么需要声明多个指针函数,但是指针函数的声明有点复杂,怎么解决?
#include<iostream>
using namespace std;
typedef void (*functionPtr)(float);
void printStuff(float){
cout << "1" << endl;
}
int main()
{
functionPtr f1,f2,f3;
printStuff(12.3);
f1=printStuff;
f1(15.6);
f2=printStuff;
f2(11.3);
f3=printStuff;
f3(1.6);
rerurn;
}
6.2.11 对象指针
1.声明指针对象的一般语法
类名 *对象指针名;
实例:
Point *ptr;
Point p1;
ptr=&p1;
2.访问指针对象的成员的语法
对象指针名->成员名
//等价于
(*对象指针名).成员名
- 对象指针在使用之前,也一定要先进行初始化,让它指向一个已经声明过的对象,然后再使用。
3.this指针
- this 指针是一个隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数和析构函数),它用于指向正在被成员函数操作的对象。
- this 指针实际上是类成员函数的一个隐含参数。在调用类的成员函数时,目的对象的地址会自动作为该参数的值,传递给被调用的成员函数,这样被调函数就能够通过 this 指针来访问目的对象的数据成员。
//在类的某一成员函数中
return x; //返回类的数据成员
//等价于
return this->x;
- this 指针明确指出了成员函数当前所操作的数据所属的对象。
- this 是一个指针常量,对于成员函数,this 同时又是一个指向常量的指针。
4.指向类的非静态成员的指针
4.1指向类的非静态成员(数据成员)的指针
指向对象成员的指针使用前也要先声明,再赋值,然后引用。
(1)声明指向类的非静态成员(数据成员)的指针的语法
类型说明符 类名::*指针名;
(2)声明了指向成员的指针之后,需要对其进行赋值,也就是要确定指向类的哪一个成员。对数据成员指针赋值的一般语法:
指针名=&类名::数据成员名;
(3)访问数据成员的方式
- 对于一个普通变量,用"&"运算符就可以得到它的地址,将这样的地址赋值给相应的指针就可以通过指针访问变量。但是对于类的成员来说问题就要稍微复杂些。
- 类的定义只确定了各个数据成员的类型、所占内存大小以及它们的相对位置,在定义时并不为数据成员分配具体的地址。因此经上述赋值之后,只是说明了被赋值的指针是专门用于指向哪个数据成员的,同时在指针中存放该数据成员在类中的相对位置(即相对于起始地址的地址偏移最),当然通过这样的指针现在并不能访问什么。
- 由于类是通过对象而实例化的,在声明类的对象时才会为具体的对象分配内存空间,这时只要将对象在内存中的起始地址与成员指针中存放的相对偏移结合起来就可以访问到对象的数据成员了。
对象名.*类数据成员指针名
对象名->*类数据成员指针名
4.2 指向类的非静态成员(函数成员)的指针
(1)声明指向类的非静态成员(函数成员)的指针的语法
类型说明符 (类名::*指针名)(参数表);
(2)对函数成员指针赋值的一般语法:
指针名=&类名::函数成员名;
(3)访问函数成员的方式
- 一个普通函数的函数名就表示它的起始地址,将起始地址赋给指针,就可以通过指针调用函数。
- 类的成员函数虽然并不在每个对象中复制一份副本,但是由于需要确定 this 指针,因而必须通过对象来调用非静态成员函数。
- 因此经过上述对成员函数指针赋值以后,也还不能用指针直接调用成员函数,而是需要首先声明类的对象,然后用以下形式的语句利用指针调用成员函数。
(对象名.*类函数成员指针名)(参数表)
(对象指针名->*类函数成员指针名)(参数表)
5.指向类的静态成员的指针
- 对类的静态成员的访问是不依赖于对象的,因此可以用普通的指针来指向和访问静态成员。
6.3 动态内存分配
1.动态内存分配的语法
new 数据类型(初始化参数列表);
- 该语句在程序运行过程中申请分配用于存放指定类型数据的内存空间,并根据初始化参数列表中给出的值进行初始化。
- 如果内存申请成功,new 运算便返回一个指向新分配内存首地址的类型的指针,可以通过这个指针对堆对象进行访问;如果申请失败,会抛出异常。
2.实例
int *point;
point = new int(2)
(1)对于基本数据类型,如果不希望在分配内存后设定初值,可以把括号省去。
int *point = new int;
(2)如果保留括号,但括号中不写任何数值 ,则表示用0对该对象初始化。
int *point = new int();
3.实例:建立的对象是某个类的实例对象
(1)有用户自定义的构造函数
Point *p1 = new Point(参数表)
(2)有用户自定义的默认构造函数
- 在用 new 建立一个类的对象时,如果该类存在用户定义的默认构造函数 ,则"new T"和"new T ( )"这两种写法的效果是相同的 ,都会调用这个默认构造函数。
(3)用户未自定义默认构造函数
- 使 用" new T "创建对象时,会调用系统生成的隐含的默认构造函数;
- 使 用" new T ( ) "创建对象时,系统除了执行默认构造函数会执行的那些操作外,还会为基本数据类型和指针类型的成员用 0 赋初值,而且这一过程是递归的。也就是说,如果该对象的某个成员对象也没有用户定义的默认构造函数,那么对该成员对象的基本数据类型和指针类型的成员 ,同样会被以 0 赋初值。
4.释放创建的动态内存空间
delete 指针名;
- 用 new 分配的内存 ,必须用 delete 加以释放,否则会导致动态分配的内存无法回收,使得程序占据的内存越来越大,这叫做“内存泄漏"。
5.用new创建数组类型的对象
(1)new运算符动态创建一维数组的语法
new 类型名[数组长度];
- 用 new 动态创 建一维数组时,在方括号后仍然可以加小括号“( )”,但小括号内不能带任何参数。
- 是否加“( )”的区别在于:
- 不加“( )”则对数组每个元素的初始化,与执行" new T"时所进行初始化的方式相同;
- 加“( )”,则与执 行 "new T ( ) " 所进行初始化的方式相同。
6.删除动态创建的数组对象
delete[] 指针名;
6.4 用 vector 创建数组对象
vector 不是一个类,而是一个类模板。
1.用 vector 定义动态数组的语法
vector<元素类型>数组对象名(数组长度);
实例:
int x=10;
vector<int>array(x);
2.初始化问题
- 与普通数组不同的是 ,用 vector 定义的数组对象的所有元素都会被初始化。
- 如果数组的元素类型为基本数据类型 ,则所有元素都会被以 0 初始化;
- 如果数组元素为类类型 ,则会调用类的默认构造函数初始化。
- 因此如果以此形式定义的 vector 动态数组, 需要保证作为数组元素的类具有默认构造函数。
- 初值也可以自己指定,但只能为所有元素指定相同初值。
vector<元素类型>数组对象名(数组长度,元素初值);
3.访问问题
- 对 vector 数组对象元素的访问方式,与普通数组具有相同的形式
数组对象名[下标表达式]
4.重要信息
- vector 数组对象的名字表示的就是一个数组对象 ,而非数组的首地址 ,因为数组对象不是数组,而
是封装了数组的对象 。
6.5 深复制与浅复制
6.6 字符串
6.6.1 用字符数组存储和处理字符串
1.字符串常量
- 字符串常最是用一对双引 号括起来的字符序列。
- 例如,“abcd”、“China”、“Happy!”
- 它在内存中的存放形式是,按串中字符的排列次序顺序存放 ,每个字符占一个字节 ,并在末尾添加 \0 作为结尾标记。
- 这实际上是一个隐含创建的类型为 char 的数组,一个字符串常量就表示这样一个数组的首地址。
- 因此,可以把字符串常量赋给字符串指针,由于常量值是不能改的,应将字符串常量赋给指向常量的指针。
const char* str = "This is a string."
2.字符串变量
- 字符串变量也可以采用类似字符串常量的表示方式
- 如果创建一个 char 数组,每个元素存放字符串的一个字符,在末尾放置一个 \0 便构成了C ++ 字符串。
- 它的存储方式与字符串常量无异,但由千它是程序员创建的数组,因此可以改写其内容,因而这就是字符串变量而非常量了。
char str[6] = {'h','a','p','p','y','\0'};
char str[6] = "happy";
char str[] = {'h','a','p','p','y','\0'};
- 要注意,用于存放字符串的数组其元素个数应该不小于字符串的长度(字符个数)加1 。
- 对字符数组进行初始化赋值时,初值的形式可以是以逗号分隔的 ASCII 码或字符常量 ,也可以是整体的字符串常量(这时末尾的 \0 是隐含的)。
6.6.2 string类
标准 C++ 库中预定义了 string 类
- string 类提供了对字符串进行处理所需要的操作。
- 使用 string 类需要包含头文件 string。
#include<string>
- string 类封装了串的属性并提供了一系列允许访问这些属性的函数。
1.对 string 类的构造函数的研究
假装研究
-
由于 string 类具有接收 const char* 类型的构造函数,因此字符串常量和用字符数组表示的字符串变量都可以隐含地转换为 string 对象。
-
例如,可以直接使用字符串常量对 string 对象初始化。
-
string str = "Hello world!";
-
2.string 类的操作符
3.常用成员函数功能简介
string append (const char* s);
//将字符串s添加在本串尾
string assign (const chat* s);
//赋值,将s所指向的字符串赋值给本对象
int compare(const string &str) const;
//比较本串于str中的串的大小,当本串<str串时,返回负数;当本串>str串时,返回正数;当两串相等,返回0
string &insert (unsigned int p0,const char* s);
//将s所指向的字符串插入本串中位置p0之前
string substr(unsigned int pos,unsigned int n) const;
//取子串,取本串中位置pos开始的n个字符,构成新的string类对象作为返回值
unsigned int find(const basic_string &str) const;
//查找并返回str在本串中第一次出现的位置
unsigned int length() const;
//返回字符串的长度(字符个数)
void swap(string &str)
//将本串与str中的字符串进行交换
4.getline( )的用法
(1)cin.getline(字符数组名St, 字符个数N, 结束符);
功能:一次连续读入多个字符(可以包括空格),直到读满N个,或遇到指定的结束符(默认为’\n’)。读入的字符串存放于字符数组St中。读取但不存储结束符。
(2)getline(cin,字符串标识符,结束符)
getline(cin,s2) //从cin接收字符串,默认只以换行符作为分隔符
getline(cin,s2,',') // 从cin接收字符串,只以逗号作为分隔符
5.
cin.get(字符数组名St, 字符个数N, 结束符);
功能:一次连续读入多个字符(可以包括空格),直到读满N个,或遇到指定的结束符(默认为’\n’)。读入的字符串存放于字符数组St中。既不读取也不存储结束符。