- 6.1 数组类型
一、一维数组
1 声明
语法:
<数据类型> <数组名> [<整型常量表达式>];
注意:
数据类型是数组元素的数据类型;整形常量表达式必须是正整数;
变量应遵循“先声明后使用”的原则。
2 使用
语法:
<数组名> [<整型表达式>];
注意:
整型表达式是下标;数组元素下标从0开始;使用数组元素与使用一般变量没有区别;
数组声明中[]的长度与使用元素时[]的下标是不同概念,前者是常量,后者是常量或变量;
数组名就是该数组的起始地址,是一个地址常量。
3 初始化
在声明数组的同时对其元素进行初始化,使用花括号{}将初始化值括起来;
初始化时,也可不指明数组长度,编译器将用初始化列表的长度作为数组长度。
注意:
若数组长度很长,且每个元素都初始化为0,可简化成:
int num[100]={0};
4 整体操作
数组不能进行整体赋值,因为数组名是常量,不能被赋值;
除char类型的数组外,其余类型数组不能整体输入和输出。
5 作函数参数
数组作为参数时,需要指定数组,还需要指定数组长度;
选取数组名作为函数实参,选取类似数组定义方式作为形参。
举例:
#include <iostream>
using namespace std;
void input(float [], int); // 函数声明,或写成 void input(float array[], int len);
void getHighest(float [], int, float&);
const int NUM=20;
int main()
{
float score[NUM];
float highest;
input(score,NUM); // 通过score传递数组首地址,通过NUM传递数组长度给input函数的响应形参
getHighest(score,NUM,highest);
cout<<highest<<endl;
return 0;
}
void input(float array[], int len) // 函数定义
{
int i;
for(i=0;i<len;i++)
{
cin>>array[i];
}
}
void getHighest(float array[], int len, float& max)
{
int i;
for(i=0;i<len;i++)
{
if(array[i]>max)
max=array[i];
}
}
6 有字面意义的下标——枚举类型作下标
枚举类型作下标,可增强代码可读性。
枚举类型变量可以与整型变量进行预算,结果为整型;
但整型变量不能直接赋值给枚举类型变量,需要强制类型转换。
7 利用typedef定义一维数组
举例:
typedef int Array[5];
Array arr; // 等价于 int arr[5];
注意:
Array是数据类型名,不是数组名,表示长度为5、元素类型为int的数据类型。
用Array定义的数据arr是长度为5、元素类型为int的一维数组。
8 结构数组
const int NUM=100;
struct Date{
int year;
int month;
int day;
};
struct StudentRec{
string name;
int number;
char gender;
Date Birth;
};
StudentRec EngClass[100];
cout<<EngClass[0].Birth.year<<" "<<EngClass[0].Birth.month<<" "<<EngClass[0].Birth.day<<endl;
9 对象数组
创建和使用对象数组与结构数组类似。
二、二维数组
1 声明
语法:
<数据类型> <数组名> [<整型常量表达式>][<整型常量表达式>];
注意:二维数组声明中的长度必须是常量。
2 使用
语法:
<数组名> [<整型表达式>][<整型表达式>]
使用二维数组元素和使用一般变量没有区别;
3 初始化
声明二维数组的同时,对其元素初始化;
每一行元素用一对花括号括住;
若初始化列表中给出所有元素的初始值,则声明时可省略对行数的说明;
若初始化列表中给出的初始化数目少于元素数目,则其余元素均初始化为0。
举例:
int arr[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; // 等价于 int arr[][4]={...};
4 枚举类型作二维数组元素下标
可增强代码可读性;
5 二维数组的一般处理
处理通常有:遍历整个数组、处理行元素、处理列元素等;
举例:
#include <iostream>
using namespace std;
const int CITYS = 9;
const int MONTHS = 12;
int main()
{
float avgTemp[CITYS][MONTHS];
float sum = 0;
int i, j;
// 求二维数组中所有元素之和
for (i = 0; i < CITYS; i++)
{
for (j = 0; j < MONTHS; j++)
{
cin >> avgTemp[i][j];
sum += avgTemp[i][j];
}
}
cout << "The sum is:" << sum << endl;
// 求二维数组每行之和
float sum1[CITYS] = { 0 };
for (i = 0; i < CITYS; i++)
{
for (j = 0; j < MONTHS; j++)
{
sum1[i] += avgTemp[i][j];
}
}
for (i = 0; i < CITYS; i++)
{
cout << "The sum of row " << i << " is: " << sum1[i] << endl;
}
// 求二维数组每列之和
float sum2[MONTHS] = {0};
for (j = 0; j < MONTHS; j++)
{
for (i = 0; i < CITYS; i++)
{
sum2[j] += avgTemp[i][j];
}
}
for (j = 0; j < MONTHS; j++)
{
cout << "The sum of col " << j << " is: " << sum2[j] << endl;
}
return 0;
}
6 定义二维数组的其他方法
方法1:
typedef float MonthType[12];
MonthType avgTemp[9];
二维数组avgTemp可看成一维数组:该一维数组的元素分别是avgTemp[0...8],avgTemp[0...8]这9个元素都是长度为12的、元素类型为float的一维数组。
方法2:
把行数相等、列数相等、元素类型相同的二维数组堪称属于某种数据类型。
这样只需创建出这种数据类型,就可用其定义多个二维数组。
typedef float ArrayType[9][12];
ArrayType avgTemp;
其中,ArrayType是数据类型名,表示“行列数目分别为9、12的、数据类型为float的”形式的二维数组的数据类型;
用ArrayType定义的avgTemp等价于:
float avgTemp[9][12];
7 数组作函数参数
二维数组名可作实参;
被调用函数相应的形参必须给出第二维的长度(列数),且该长度必须与实参的列长度相等;
无需给出第一维的长度(行数),添加一个形参接收传递进来的行数。
const int CITYS=9;
const int MONTHS=12;
... ...
input(avgTemp,CITYS); // 调用input函数
... ...
void input(float array[][MONTHS], int row) // 定义input函数
{... ...}
const int CITYS=9;
const int MONTHS=12;
typedef float Array2D[CITYS][MONTHS];
typedef float Array1D[CITYS];
... ...
Array2D avgTemp;
Array1D highest;
getHighest(avgTemp,highest);
... ...
void getHighest(Array2D array, Array1D largest)
{... ...}
注意两个例子之间的区别。
- 6.2 指针类型
一、基本概念
指针,即地址,包含两方面信息:1 地址值;2 所指向数据的数据类型。
二、指针常量与指针变量
1 指针常量与取地址操作符&
C++提供取地址符&,可置于某变量名前,用以获取存放该变量的内存块地址。
举例:
int i;
注意:定义语句后,&i是i的地址,是指向int型变量i的指针,是一个指针常量。
2 指针变量的定义、运用及指针操作符*
2.1 指针变量的定义、运用
定义指针变量时不仅要说明该变量是指针变量,还要说明该指针变量所指向数据的数据类型。
语法:
<数据类型>* <指针变量名>;
<数据类型> *<指针变量名>;
注意:在同一语句中定义多个指针变量时,应采用第二种形式。
2.2 指针操作符*
C++提供指针操作符*,置于指针变量前,获取该指针变量所指向的变量。
注意:
不要混淆定义指针变量的语句中的*与指针操作符*,前者说明是指针变量,后者用以获取指针所指向的变量;
一定先为指针变量赋值,使其指向某一确切的内存块后,才可使用指针操作符*访问该内存块。
3 直接访问、间接访问
从编译器的角度,间接访问是使用变量名访问变量的方式;直接访问是使用地址访问变量的方式。
4 (void*)类型的指针
一般情况下,指针包含两个信息:地址值和所指向数据的数据类型;
特殊地,指针只包含地址值,不包含所指向数据的数据类型,该指针就是指向void型的指针。
语法:
void* <指针变量名>;
一开始,把某段内存的起始地址赋值给指针变量,但所指向数据的数据类型尚未确定,需要在之后进行强制类型转换,得到指向某种确定类型的指针后才可使用。
三、指针的运用
1 修改指针变量的值,使之指向另一变量
指针变量是变量,其值可修改;若修改,则其指向另一变量。
#include <iostream>
using namespace std;
int main()
{
int a = 5, b = 9;
int *p1, *p2, *p;
p1 = &a; // p1指向变量a
p2 = &b; // p2指向变量b
p = p1; // p1与p2的值互换
p1 = p2;
p2 = p;
cout << a << "\t" << b << endl;
cout << *p1 << "\t" << *p2 << endl;
system("pause");
return 0;
}
2 指针变量作函数参数
2.1
#include <iostream>
using namespace std;
void swap(int* p1, int* p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 4, b = 5;
int *p1, *p2;
p1 = &a;
p2 = &b;
swap(p1, p2);
cout << a << "\t" << b << endl; // 两个值交换
system("pause");
return 0;
}
传递方式是值传递,即实参p1的值单向传递给形参p1,实参p2的值单向传递给形参p2,从而形参p1和p2的值分别是a和b的地址;
在swap函数中,p1和p2也指向a和b,*p1是a,*p2是b;而swap函数中的三条赋值语句实质是a和b的值对调。
注意:
main函数的实参p1与swap函数的形参p1是不同的两个变量,两者关系只存在于main调用swap时,实参p1的值传递给形参p1,之后就没有任何关系。
相当于向main函数返回多个结果值,这是指针做参数的优点。
2.2
#include <iostream>
using namespace std;
void swap(int* p1, int* p2)
{
int *temp;
*temp = *p1;
*p1 = *p2;
*p2 =*temp;
}
int main()
{
int a = 4, b = 5;
int *p1, *p2;
p1 = &a;
p2 = &b;
swap(p1, p2);
cout << *p1 << "\t" << *p2 << endl;
system("pause");
return 0;
}
运行该程序时,出现”内存访问非法“的错误,因为定义temp为指针变量后,没有赋值给它确切的地址值;
因此,temp随机指向内存空间中某个位置,即*temp为内存空间中某一不确定的内存块;
强行把a的值赋值给*temp,导致内存访问非法。
2.3
#include <iostream>
using namespace std;
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
int main()
{
int a = 4, b = 5;
swap(a, b);
cout << a << "\t" << b << < endl; // 两个值未交换
return 0;
}
运行后,两个值未调换,因为实参a和形参x是两个不同的变量,两者唯一关系是在调用swap函数时,a的值单向传递给x,之后没有任何关系;
因此无论x在swap函数中怎么修改,都不会影响main函数中的a。
2.4
#include <iostream>
using namespace std;
void swap(int* p1, int* p2)
{
int *temp;
temp = p1;
p1 = p2;
p2 = temp;
}
int main()
{
int a = 4, b = 5;
int *p1, *p2;
p1 = &a;
p2 = &b;
swap(p1, p2);
cout << *p1 << "\t" << *p2 << endl; // 两个值未交换
system("pause");
return 0;
}
形参p1和实参p2是两个不同的变量,因此在swap中形参p1与p2的值对调,对于实参p1和p2以及a和b都没有任何影响。
3 指针与引用
3.1 相同点
使一个函数向调用者传递多个结果值;
3.2 不同点
原理不同:对于引用传递参数,实参和形参是同一个变量;对于指针传递参数,被调用函数获取某变量的地址,从而使用该地址访问该变量;
使用引用形参比使用指针参数方便,因为使用指针时,要明确指针指到哪里;对于资深程序员,更喜欢指针。
#include <iostream>
using namespace std;
void swap(int& p1, int& p2)
{
int temp;
temp = p1;
p1 = p2;
p2 = temp;
}
int main()
{
int a = 4, b = 5;
int *p1, *p2;
p1 = &a;
p2 = &b;
swap(p1, p2);
cout << *p1 << "\t" << *p2 << endl;
system("pause");
return 0;
}
- 6.3 指针类型与数组
一、通过指针引用数组元素
1 数组名
数组名是地址常量(指针常量),是该数组的起始地址,或该数组第0个元素的地址。
如:
float score[5];
float* p = score; // 定义指向float型的指针变量,并把score赋值给该变量,则p指向score[0]
float* p = &score[0]; // 也可把score[0]的地址&score[0]赋值给p
2 通过指针使用元素
2.1 下标法: a[i]
2.2 指针法: *(a+i) 或 *(++a)
二、数组作函数参数的进一步讨论
#include <iostream>
using namespace std;
const int NUM = 5;
void sort(int* arr, int n) // 选择排序
{
int i, j, k, t;
for (i = 0; i < n - 1; i++)
{
k = i; // 找到[i...n-1]之间的最大元素,用k记录其下标
for (j = i + 1; j < n; j++)
{
if (*(arr + k) < *(arr + j))
k = j;
}
if (k != i)
{
t = arr[i];
arr[i] = arr[k];
arr[k] = t;
}
}
}
int main()
{
int *p, n[NUM];
for (p = n; p < n + NUM; p++)
cin >> *p;
sort(n, NUM);
for (p = n; p < n + NUM; p++)
cout << *p << " ";
system("pause");
return 0;
}
三、动态分配内存
定义数组时必须指定数组长度;
但可对内存进行动态分配,即在程序运行时根据实际需要分配内存空间。
1 malloc和free
int* p=(int*)malloc(len*sizeof(int));
free(p);
注意:
malloc函数返回值是void*型,无法用下标或指针法使用元素,也不能用指针作自增运算,因为这些运算需要知道指针所指向的数组的类型,因此需要把返回值转换为指向某种数据类型的指针。
malloc函数只负责分配len个字节的内存块,并返回该内存块的起始地址;对于该内存块内部如何划分为一个个元素,取决于把malloc的返回值转换为指向何种类型数据的指针。
2 new和delete
int* p=new int[len];
delete[] p;
注意:
使用new操作也可为存储单个数据分配内存块,则在数据类型后无需[len]部分;delete也不需要使用[]。
int* p=new int;
delete p;
malloc和free一般用于简单数据类型的动态内存分配;对于对象的动态内存分配,则需要使用new和delete。
四、二维数组与指针
1 二维数组名与指向指针的指针
int a[3][4]={{1,2,3,4},{2,3,4,5},{3,4,5,6}};
a[i]和a都是指针,但指向的数据类型不同。
a[i]指向一个元素;a指向一行元素(准确地说,a指向一维数组)。
int (*p)[4]; // 括号必不可少
p=a;
定义p为指向“由4个int型元素组成的一维数组”的指针。
cout<<p[1][2]<<endl;
cout<<*(*(p+1)+2)<<endl;
2 动态分配内存
#include <iostream>
using namespace std;
void input(float** score, int StuNum, int CouNum)
{
int i, j;
for (i = 0; i < StuNum; i++)
{
for (j = 0; j < CouNum; j++)
{
cin >> score[i][j];
}
}
}
void average(float** score, int StuNum, int CouNum, float* avg)
{
int i, j;
float sum;
for (i = 0; i < StuNum; i++)
{
sum = 0;
for (j = 0; j < CouNum; j++)
{
sum += score[i][j];
}
avg[i] = sum / CouNum;
}
}
void print(float* avg, int StuNum)
{
int i;
for (i = 0; i < StuNum; i++)
{
cout << avg[i] << endl;
}
}
int main()
{
float** score;
float* avg;
int StuNum, CouNum, i;
cin >> StuNum >> CouNum;
score = new float*[StuNum]; // 分配行指针内存
for (i = 0; i < StuNum; i++) // 分配每行元素内存
{
score[i] = new float[CouNum];
}
avg = new float[StuNum];
input(score, StuNum, CouNum);
average(score, StuNum, CouNum, avg);
print(avg, StuNum);
for (i = 0; i < StuNum; i++) // 释放每行元素内存
delete[] score[i];
delete[] score; // 释放行指针内存
delete[] avg;
system("pause");
return 0;
}
注意:
动态分配二维数组内存空间需要两步:分配StuNum个元素,每个元素是指向float型数据的指针;循环里动态分配StuNum个一维数组,每个数组长度都是CouNum。
score是float**型变量,即score是一个指向指针的指针变量,指向一个一维的指针数组。
其中每个元素是一个float*型的指针,指向一个元素为float型的一维数组。
- 6.4 指向结构变量的指针
#include <iostream>
#include <string>
using namespace std;
struct student{
long num;
string name;
char sex;
float score;
};
int main()
{
student stu;
student* p; // 指向结构变量的指针变量与指向简单数据类型的指针变量的定义是一样的
p = &stu;
stu.num = 1000; // 通过变量名访问成员使用"."操作符
stu.name = "Linda";
stu.sex = 'F';
stu.score = 97.5;
cout << p->score << endl; // 通过指针访问成员使用"->"操作符
cout<< p->name << endl << p->sex << endl << p->score << endl;
return 0;
}
- 6.5 对象指针
对象的创建和撤销都是由系统自动完成的。
但对象是一种复合型数据,往往占用较多内存空间;若程序中需要使用很多对象,易造成内存紧张,因为有的对象在某时刻后可能已经不再使用,但直到生命期结束才被撤销,白白浪费内存。
解决办法:程序需要时创建对象,使用完毕立即撤销对象。即对象的撤销不是等待生命期结束时由系统自动撤销,而是由程序员通过程序语句撤销。
这种方式称为对象的动态创建与撤销,使用指向对象的指针实现。
1 对象的动态创建和撤销
使用new和delete动态创建和撤销对象。
#include <iostream>
using namespace std;
int main()
{
Date* datePtr; // 声明一个指向Date类对象的指针,但该指针指向哪里暂时还没确定
datePtr = new Date(1988, 3, 25); // 使用new动态创建一个Date类对象,并把该对象的地址赋值给datePtr
// 为一个Date类对象分配内存,并根据初始化列表中的实参调用Date类中适当的构造函数对该对象进行初始化,再将该内存块的地址返回赋值给datePtr
if (datePtr == NULL)
cout << "内存分配失败。" << endl;
else
{
datePtr->increment();
datePtr->print();
}
delete datePtr;
return 0;
}
利用new操作动态创建对象的一般形式为:
<对象指针>=new <类名>(初始化列表);
初始化列表中给出的是传递给构造函数的实参。若类没有构造函数或只有默认构造函数,则圆括号和初始化列表军部出现。
由new分配内存不一定成功。若new分配内存失败,则返回NULL值。因此程序中应该先判断内存分配是否成功,再进行相应处理;若内存分配成功,则利用对象指针和"->"操作符访问对象的公有成员。
使用delete操作撤销对象。使用delete时,调用该对象的析构函数。
注意:delete操作的操作数是指针,delete一个指针等价于释放掉该指针所指向的内存块;在C++中同一个内存块的释放只需进行一次,已经delete的指针再次delete会产生错误。因此,最好不要让多个指针指向同一个对象。
程序不再需要动态创建对象,一定要记住撤销该对象;否则该内存块会一直被占用,无法分配给别的程序使用。若指向该内存块的指针指向别处,将无法回收这些内存块,称这些无法回收的内存块为内存垃圾。
二、对象的复制
1 浅复制
一个对象可以直接赋值给相同类型的另一个对象,结果是一个对象中的数据成员分别赋值给另一个对象相应的数据成员。
当数据成员不出现指针时,没有任何问题;但若一些数据成员是指针变量,就会出现“指针别名”和“内存垃圾”现象。
举例:
class Animal{
public:
Animal(char* str)
{
name = new char[strlen(str) + 1];
strcpy(name, str);
......
}
~Animal()
{
delete name;
......
}
private:
char* name;
};
Animal a1("cat");
Animal a2("dog");
a2 = a1;
复制后,a2中的指针name获得与a1中的name相同的值,也指向字符串"cat",此时字符串"dog"成为内存垃圾;
若两个对象中任一个释放了name指针,另一个对象的name失效。
2 深复制
- 6.6 函数指针
函数是代码集合,存放在内存空间中某区域内;函数名是该区域的入口地址。
调用函数可通过函数名;也可将函数名赋值给一个指向函数的指针变量,再通过该指针变量调用函数。
#include <iostream>
using namespace std;
int min(int a, int b)
{
return (a < b ? a : b);
}
int main()
{
int(*p)(int, int); // 定义一个指向函数的指针
p = min; // min赋值给p,p指向min函数
cout << (*p)(3, 4) << endl; // (*p)是函数名本身,与min(3,4)等价
system("pause");
return 0;
}
1 指向函数的指针变量定义形式:
<数据类型> (*<指针变量名>)(形参列表)
其中,数据类型是指针变量指向的函数的返回值类型。
2 指针变量明确指向某个函数后,才可使用该指针变量
3 上例中p是“指向函数”的指针,即p只能指向函数的入口处,并非指向函数中的某条指令
4 p也可指向其他函数,但这些函数必须是“返回值类型是int,且两个形参都是int类型”;p不能指向返回值类型或形参列表不同的其他函数