目录
例.设有一个int型数组a,有10个元素。用三种方法输出各元素
数组
数组的初始化
1、当指定的初值个数小于数组大小时,剩下的数组元素会被赋予0值。
2、若定义数组时没有指定任何一个元素的初值,对于静态生存期的数组,每个元素仍然会被赋予0值;但对于动态生存期的数组,每个元素的初值都是不确定的。
3、如果给出全部元素的初值,第一维的下标个数可以不用显示说明。
数组作为函数参数
如果使用数组名作函数的参数,则实参和形参都应该是数组名(形参还要带数组大小),且类型要相同。和普通变量做参数不同,使用数组名传递数据时,传递的是地址,因此如果在被调函数中对形参数组元素值进行改变,主调函数中实参数组的相应元素值也会改变。
注意:
(1)实参数组的元素个数不应该少于形参数组的元素个数。
(2)把数组作为参数时,一般不指定数组第一维的大小,即使指定,也会被忽略。
对象数组
数组的元素不仅可以是基本数据类型,也可以是自定义类型。如果在声明对象数组时给每一个数据元素指定初始值,就会调用与形参类型相匹配的构造函数;如果没有指定数组元素的初始值,就会调用默认构造函数。
指针
内存空间的访问方式
地址编码的基本单位是字节,每个字节由8个二进制位组成,即每个字节是一个基本内存单元,有一个地址。
具有静态生存期的变量在程序开始运行之前就已经被分配了内存空间;具有动态生存期的变量是在程序运行时遇到变量声明语句时被分配内存空间的。在变量获得内存空间的同时,变量名也就成了相应内存空间的名称,在变量的整个生存期内都可以用这个名字访问该内存空间,表现在程序语句中就是通过变量名存取变量内容。
有时使用变量名不够方便或者根本没有变量名可用(比如动态分配的内存单元),这时只能通过地址访问,指针就派上了用场。
指针变量的声明
指针也是一种数据类型,具有指针类型的变量称为指针变量。指针变量用于存放内存单元的地址。通过变量名访问一个变量是直接的,通过指针访问一个变量是间接的。指针也是先声明后使用,声明指针的语法形式:
数据类型 *标识符;
为什么声明指针变量时要指出它所指的对象是什么类型的呢?(1)声明了变量需要的内存空间;(2)限定了对变量可以进行的运算及其运算规则。
与地址相关的运算“*”和“&”
“*”称为指针运算符,也称为解析,表示获取指针所指向的变量的值。
“&”称为取地址运算符,用来得到一个对象的地址。
必须注意,“*”和“&”出现在声明语句和执行语句中其含义是不同的,它们作为一元运算符和二元运算符时含义也是不同的:
(1)“*”出现在声明语句中,在被声明的变量名之前时,表示声明的是指针。
int *p;
(2)“*”出现在执行语句中或声明语句的初始化表达式中作为一元运算符,表示访问指针所指对象的内容。
cout << *p ;
(3)“&”出现在变量声明语句中位于被声明的变量左边时,表示声明的是引用。
int &rf;
(4)“&”在给变量赋初值时出现在等号右边或在执行语句中作为一元运算符出现时,表示取对象的地址。
int a, b;
int *pa = &a, *pb;
pb = &b;
指针的赋值
定义指针之后必须先赋值才可以引用,否则其中的地址值是一个不确定的数,即野指针。
存储类型 数据类型 *指针名=初始地址;//定义的同时进行初始化赋值
指针名=地址;//单独使用赋值语句
关于指针的类型,还应注意下几点:
(1)指向常量的指针:不能通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象。
int a;
const int *p=&a;//const修饰int
int b;
p=&b;//正确
*p=1;//错误
(2)指针类型的常量:指针本身的值不能被改变。
int *const p=&a;//const修饰p
p=&b;//错误
(3)指针类型的函数
数据类型 * 函数名(参数表){
函数体
}
(4)指向函数的指针
数据类型 (*函数指针名)(形参表)
(5)引用和指针的区别:
引用时一个别名,不能为NULL值,不能被重新分配;指针是一个存放地址的变量。当需要对变量重新赋以另外的地址或赋值为NULL时,只能使用指针。
(6)数组名称实际上就是一个不能被赋值的指针,即指针常量。
(7)可以使多个指针指向同一个变量。
(8)一般情况下,指针的值只能赋给相同类型的指针。特殊的是,void类型指针,可以存储任何类型的对象地址。经过使用类型显式转换,通过void类型的指针便可以访问任何类型的数据。void指针一般只在指针所指向的数据类型不确定时使用。
(9)数据成员是指针类型,赋值要用动态分配。
指针运算
*(p+n)表示p当前所指位置后方第n个数的内容,它也可以写作p[n];同样,*(p-n)也可以写作p[-n]。
不同类型的指针之间或指针与非0整数之间的关系运算是毫无意义的。但是指针变量可以和整数0比较,0专用于表示空指针,也就是一个不指向任何有效地址的指针。赋给指针变量的值必须是地址常量或地址变量,不能是非0整数,但可以给一个指针变量赋值为0,这时表示整改指针是一个空指针,不指向任何地址。例如:
int *p=0;//空指针,也可以用NULL表示
注:NULL是一个在很多头文件都有定义的宏,被定义为0。
用指针处理数组元素
把数组作为函数的形参,等价于把指向数组元素类型的指针作为形参。以下三种形参列表的写法是等价的:
void f(int p[]);//第一维长度可以省略
void f(int p[3]);//标准
void f(int *p);//指向数组元素的指针
例.设有一个int型数组a,有10个元素。用三种方法输出各元素
#include<iostream>
using namespace std;
int main() {
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++)//法一:使用数组名和下标
cout << a[i] << " ";
cout << endl;
for (int i = 0; i < 10; i++)//法二:使用数组名和指针运算
cout << *(a + i) << " ";
cout << endl;
for (int *p = a; p < (a + 10); p++)//法三:使用指针变量
cout << *p << " ";
cout << endl;
return 0;
}
指针数组
如果一个数组的每个元素都是指针变量,这个数组就是指针数组。指针数组的每个元素必须是同一类型的指针。声明一维指针数组的语法形式为:
数据类型 *数组名[下标表达式];
例.利用指针数组输出单位矩阵
#include<iostream>
using namespace std;
int main() {
int line1[] = { 1,0,0 };
int line2[] = { 0,1,0 };
int line3[] = { 0,0,1 };
int *p[3] = { line1,line2,line3 };//三个指针分别指向三个一维数组,所以自身变成二维数组
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++)
cout << p[i][j] << " ";
cout << endl;
}
system("pause");
return 0;
}
上例中p[i][j]与*(p[i]+j)或*(*(p+i)+j)等价,即先把指针数组p所存储的第i个指针读出,然后读取它所指向的地址的后方第j个数。
用指针作为函数参数
如果以指针作为形参,在调用时实参将值传递给形参,也就是使实参和形参指针变量指向同一内存地址。以指针作形参有三个作用:
(1)参数双向传递;
(2)减少函数调用时数据传递的开销;
(3)通过指向函数的指针传递函数代码的首地址。
习惯:
(1)如果函数体中不需要通过指针改变指针所指向对象的内容,应在参数表中将其声明为指向常量的指针,这样使得常对象被取地址后也可作为该函数的参数。
(2)在设计程序时,当某个函数中以指针或引用作为形参都可以达到同样目的,使用引用会使程序的可读性更好些。
例.读入1个浮点数,将整数部分和小数部分分别输出
#include<iostream>
using namespace std;
void split(float x, int *intPart, float *fracPart) {//fraction分数
*intPart = (int)x;
*fracPart = x - *intPart;
}
int main() {
float x;
int a;//整数部分
float b;//小数部分
cout << "Enter one float point number:" << endl;
cin >> x;
split(x, &a, &b);//变量地址作为实参
cout << "Integer Part=" << a << " Fracton Part=" << b << endl;
system("pause");
return 0;
}
指针类型函数
当一个函数的返回值是指针类型时,这个函数就是指针型函数。使用指针函数的最主要目的就是要在函数结束时把大量的数据从被调函数返回到主调函数中,而通常非指针函数调用结束后,只能返回一个变量或者对象。
数据类型 *函数名(参数表){
函数体
}
指向函数的指针
实际上函数名就表示这个函数的代码在内存中的起始地址(非静态成员函数除外)。
数据类型 (*函数指针名)(形参表)
数据类型后面的括号决定了它是函数指针,否则是指针类型函数。
例.函数指针实例
#include<iostream>
using namespace std;
void fun1(float) {//如果不写形参类型,后面的函数指针将无法指向这个函数,因为形参列表不对应
cout << "fun1" << endl;
}
void fun2(float data) {
cout << "fun2:" << data << endl;
}
void fun3(float data) {
cout << "fun3:" << data << endl;
}
const float PI = 3.14159f;
int main() {
void(*p)(float);//函数指针
fun1(PI);
p = fun1;
p(PI);
p = fun2;
p(PI);
p = fun3;
p(PI);
fun3(PI);
system("pause");
return 0;
}
可见,调用函数指针时不用加“*”。另外,函数指针的返回类型、形参个数、形参类型必须与其指向的函数相同,否则报错。
对象指针
类名 *对象指针名;
例如:
Point *p;
Point p1;
p=&p1;
注:对象所占据的内存空间只适用于存放数据成员的,函数成员不在每一个对象中存储副本。
通过对象指针访问成员有两种语法形式,这两种是等价的:
对象指针名->成员名
(*对象指针名).成员名
例.使用指针来访问Point类的成员
#include<iostream>
using namespace std;
class Point {
public:
Point(int x=0,int y=0):x(x),y(y){}
int getX() const { return x; }
int getY() const { return y; }
private:
int x, y;
};
int main() {
Point a(4, 5);
Point *p = &a;//说明对象名不代表对象的首地址
cout << p->getX() << endl;
cout << a.getX() << endl;
system("pause");
return 0;
}
this指针
this指针是一个隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数和析构函数),它用于指向正在被成员函数操作的对象。在成员函数中,可以使用*this来表示正在调用该函数的对象。
指向类的非静态成员的指针
声明形式为:
类型说明符 类名::*指针名; //声明指向数据成员的指针
类型说明符 (类名::*指针名)(参数表); //声明指向函数成员的指针
对指针赋值:
指针名=&类名::数据成员名;
指针名=&类名::函数成员名;
访问成员:
//访问数据成员
对象名.*类成员指针名 //形式一
对象指针名->*类成员指针名 //形式二
//调用函数成员
(对象名.*类成员指针名)(参数表) //形式一
(对象指针名->*类成员指针名)(参数表) //形式二
注:
(1)常成员函数与普通成员函数具有不同的类型,因此能够被常成员函数赋值的指针,需要在声明时明确写出const关键字。
(2)成员函数指针的声明、赋值和使用过程中的返回值类型、函数参数表一定要互相匹配。
指向类的静态成员的指针
对类的静态成员的访问是不依赖于对象的,因此可以用普通的指针来指向和访问静态成员。
例.用过指针访问类的静态数据成员
#include<iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y= 0) :x(x), y(y) {
count++;
}
Point(const Point &p) :x(p.x), y(p.y) {
count++;
}
~Point() { count--; }
int getX() const { return x; }
int getY() const { return y; }
static int count;
private:
int x, y;
};
int Point::count = 0;
int main() {
int *p = &Point::count;
Point a(4, 5);
cout << "Point A:" << a.getX() << "," << a.getY();
cout << "count=" << *p << endl;
Point b(a);
cout << "Point B:" << b.getX() << "," << b.getY();
cout << "count=" << *p << endl;
system("pause");
return 0;
}
例.通过指针访问类的静态函数成员
#include<iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) :x(x), y(y) {
count++;
}
Point(const Point&p) :x(p.x), y(p.y) {
count++;
}
~Point() { count--;}
int getX() const { return x; }
int getY() const { return y; }
static void showCount() {
cout << "count=" << count << endl;
}
private:
int x, y;
static int count;
};
int Point::count = 0;
int main() {
void (*funPtr)() = Point::showCount;
Point a(4, 5);
cout << "Point A:" << a.getX() << "," << a.getY();
funPtr();
Point b(a);
cout << "Point B:" << b.getX() << "," << b.getY();
funPtr();
system("pause");
return 0;
}
动态内存分配
动态内存分配技术可以保证程序在运行过程中按照实际需要申请适量的内存,使用结束后还可以释放,这时的存储单元也称为堆对象,申请和释放的过程称为建立和删除。
运算符new的功能是动态分配内存,或者称为动态创建堆对象。语法形式为:
new 数据类型 (初始化参数列表);
如果内存申请成功,new运算符便返回一个指向新分配内存首地址的类型的指针;如果申请失败,会抛出异常。
(1)如果建立的对象是一个基本类型变量,初始化过程就是赋值,例如:
int *p;
p=new int(2);
动态分配了用于存放int类型数据的内存空间,并将初值2存入该空间中,然后将首地址赋给指针p。如果不希望在分配内存后设定初值,可以把括号省去,例如:
int *p=new int;
如果保留括号,但括号中不写任何数据,则表示用0对该对象初始化,例如:
int *p=new int();
(2)创建的对象是某一个类的实例对象,就是要根据初始化参数列表的参数类型和个数调用该类的构造函数。
运算符delete用来删除由new创建的对象,释放指针所指向的内存空间。格式为:
delete 指针名; //注意删除的是指针名
如果被删除的是对象,该对象的析构函数将被调用。用new分配的内存,必须用delete加以释放,否则会导致动态分配的内存无法回收,使得程序占据的内存越来越大,这叫做内存泄露。
例.动态创建对象
#include<iostream>
using namespace std;
class Point {
public:
Point() :x(0), y(0) {
cout << "default constructor called" << endl;
}
Point(int x, int y) :x(x), y(y) {
cout << "constructor called" << endl;
}
~Point() {
cout << "destructor called" << endl;
}
int getX() const { return x; }
int getY() const { return y; }
void move(int newX, int newY) {
x = newX;
y = newY;
}
private:
int x, y;
};
int main() {
cout << "step one:" << endl;
Point *p = new Point;//动态创建对象,无参数列表调用自定义默构
delete p;//删除对象,自动调用析构函数
cout << "step two:" << endl;
p = new Point(1, 2);//动态创建对象,有参数列表调用有形参的构造函数
delete p;//删除对象,自动调用析构函数
system("pause");
return 0;
}
使用new也可以创建数组类型的对象,这时需要给出数组的结构说明。语法形式为:
new 类型名[数组长度];
用new动态创建一维数组时,在方括号后仍然可以加小括号“()”,但小括号内不能带任何参数。是否加小括号的区别在于,不加则对数组每个元素的初始化与执行“new T”时所进行初始化的方式相同;加则与执行“new T()”所进行初始化的方式相同。例如,可以这样动态生成一个数组:
int *p=new int[10]();
则可以方便地为动态创建的数组用0值初始化。如果是用new建立的数组,用delete删除时在指针名前要加“[]”,格式如下:
delete[] 指针名; //不管是几维,都只有一个方括号
例.动态创建对象数组
#include<iostream>
using namespace std;
class Point {
//类的定义同上
};
int main() {
Point *p = new Point[2];//创建对象数组
p[0].move(5, 10);
p[1].move(15, 20);
delete[] p;//删除整个对象数组
system("pause");
return 0;
}
assert的含义是“断言”,它是标准C++的cassert头文件中定义的一个宏,用来判断一个条件表达式的值是否为true,如果不为true,则程序会中止,并且报告出错误,这样就很容易将错误定位。
深复制与浅复制
隐含的复制构造函数并不总是适用的,因为它完成的只是浅复制。浅复制有两个弊端:
(1)两个指针指向的是同一内存地址,表面上好像完成了复制,但是并没有形成真正的副本。
(2)该空间被两次释放,于是导致运行错误。
解决这一问题的方法是编写复制构造函数,实现深复制。