第四部分:指针与函数
指针作为函数参数:使用指针作为参数的原因:1、需要数据的双向传递;2、需要传递一组数据,只传首地址运行效率比较高。
案例:
#include <iostream>
using namespace std;
void splitFloat(float x, int *intPart, float *fracPart) {
*intPart = static_cast<int>(x); //取x的整数部分
*fracPart = x - *intPart; //取x的小数部分
}
int main() {
cout << "Enter 3 float point numbers:" << endl;
for(int i = 0; i < 3; i++) {
float x, f;
int n;
cin >> x;
splitFloat(x, &n, &f); //变量地址作为实参
cout << "Integer Part = " << n << " Fraction Part = " << f << endl;
}
return 0;
}
运行结果:
注意:浮点数在c++中是近似存储的,因此,如果要比较两个浮点数的大小,不能直接使用“==”,而可以通过判断两个浮点数的差值是否小于一个足够小的数来进行判断,只要这个差值能够满足精度,那么就可以认为这两个浮点数是相等的。
案例:
#include <iostream>
using namespace std;
const int N = 6;
void print(const int *p, int n);
int main() {
int array[N];
for (int i = 0; i < N; i++)
cin>>array[i];
print(array, N);
return 0;
}
void print(const int *p, int n) {
cout << "{ " << *p;
for (int i = 1; i < n; i++)
cout << ", " << *(p+i);
cout << " }" << endl;
}
当我们只想访问指针指向的对象,而不想对该对象做任何修改时,我们可以定义指向常量的指针。通过指针只能读取指针指向的对象,不能对对象做修改。在程序设计中,一般会有最小授权的原则,比如在定义类的时候,尽量多的能够隐藏细节,不要过多的授权。
指针类型的函数:若函数的返回值是指针,该函数就是指针类型的函数。定义语法形式:存储类型 数据类型 *函数名()
{//函数体语句
}
注意:1、不要将非静态局部地址作为函数的返回值,因为非静态的局部变量在函数外就失效了,一个常见的错误就是:在子函数中定义局部变量后将其地址返回给主函数,这是非法的地址;
错误案例:
int main(){
int* function();
int* ptr= function();
*prt=5; //危险的访问!
return 0;
}
int* function(){
int local=0; //非静态局部变量作用域和寿命都仅限于本函数体内
return &local;
}//函数运行结束时,变量local被释放
因此,返回的指针要确保在主调函数中是有效、合法的地址。例如,可以将在主函数定义的数组传递给子函数,并在子函数中返回这个数组其中一个元素的地址,这就是合法有效的地址。除此之外,在子函数中通过动态内存分配new操作取得的内存地址是合法有效的,但是内存分配和释放不在同一个级别,要注意不能忘记释放,避免内存泄漏。
指向函数的指针:1、定义:存储类型数据类型(*函数指针名)(参数表);含义:函数指针指向的是程序代码的存储区的起始地址;2、函数指针的典型用途:①通过函数指针调用的函数:例如将一个函数的指针作为参数传递给另外一个函数,使得在处理相似事件的时候可以灵活地使用不同的方法;②调用者不关心谁是被调用者:只需知道存在一个具有特定原型和限制条件的被调用函数;
案例:
#include<iostream>
using namespace std;
int compute(int a, int b, int(*func)(int, int))//func是一个函数指针,指向一个返回值
//为int类型,形参为两个int类型的参数
{
return func(a, b);
}
int max(int a, int b)
{
return (a > b) ? a : b;
}
int min(int a, int b)
{
return (a < b) ? a : b;
}
int sum(int a, int b)
{
return a + b;
}
int main()
{
int a = 5, b = 10;
cout << "The maximum of A and B is:";
cout << compute(a, b, &max) << endl;//也可不写地址运算符,函数名就代表函数的首地址
cout << "The minimum of A and B is:";
cout << compute(a, b, &min) << endl;
cout << "The sum of A and B is:";
cout << compute(a, b, &sum) << endl;
system("pause");
return 0;
}
对象指针:指向一个对象的指针,定义形式:类名 *对象指针名。通过指针访问对象成员:对象指针名->成员名,相当于(*对象指针名).成员名。
this指针:隐含于类的每一个非静态成员函数中;并能够指出成员函数所操作的对象,当通过一个对象调用成员函数时,系统先将该对象的地址赋给this指针,然后调用成员函数,成员函数对对象的数据成员进行操作时,就隐含使用了this指针。例如:Point类的getX函数中的语句:return x;相当于:return this->x。
第五部分:对象的复制与移动
Vector对象:为什么需要vector?之前我们讲过可以将动态分配内存与释放内存封装在一个类里面,但是这个类只能封装一种类型的动态数组。Vector可以封装任何类型的动态数组,自动创建和删除,并且能够进行数组下标越界检查。定义语法:vector<元素类型>数组对象名(数组长度)。对数组对象的引用:vector对象名[下标表达式];获得数组长度:vector对象名.size()。
案例:
#include<iostream>
#include<vector>
using namespace std;
double average(const vector<int> &a);//声明一个求vector对象数组平均值的函数
int main()
{
unsigned n=0;
cout << "Please input the value of n:" << endl;
cin >> n;
vector <int>array(n);//使用vector对象来保存数组
for (unsigned i = 0; i < n; i++)
{
cin >> array[i];
}
cout << "Average of array is " << average(array) << endl;
return 0;
}
double average(const vector<int> &a)
{
double sum=0;
for (int v:a)//基于范围的for循环
{
sum += v;
}
return sum / a.size();//使用vector对象名.size()函数可以返回数组的长度
}
浅层复制与深层复制:
浅层复制:实现对象数据元素之间的一一对应复制。默认复制构造函数是浅拷贝。
深层复制:当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指的对象进行复制。
案例:
#include <iostream>
#include <cassert>
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;
};
//动态数组类
class ArrayOfPoints {
public:
ArrayOfPoints(int size) : size(size) {
points = new Point[size];
}
ArrayOfPoints(const ArrayOfPoints& v);//复制构造函数
~ArrayOfPoints() {
cout << "Deleting..." << endl;
delete[] points;
}
//获得下标为index的数组元素
Point &element(int index) {
assert(index >= 0 && index < size); //如果数组下标不会越界,程序中止
return points[index];
}
private:
Point *points; //指向动态数组首地址
int size; //数组大小
};
//深层复制
ArrayOfPoints::ArrayOfPoints(const ArrayOfPoints& v) {
size = v.size;
points = new Point[size];
for (int i = 0; i < size; i++)
points[i] = v.points[i];
}
int main() {
int count;
cout << "Please enter the count of points: ";
cin >> count;
ArrayOfPoints pointsArray1(count); //创建对象数组
pointsArray1.element(0).move(5, 10);
pointsArray1.element(1).move(15, 20);
ArrayOfPoints pointsArray2 = pointsArray1; //创建对象数组副本
cout << "Copy of pointsArray1:" << endl;
cout << "Point_0 of array2: " << pointsArray2.element(0).getX() << ", "
<< pointsArray2.element(0).getY() << endl;
cout << "Point_1 of array2: " << pointsArray2.element(1).getX() << ", "
<< pointsArray2.element(1).getY() << endl;
pointsArray1.element(0).move(25, 30);
pointsArray1.element(1).move(35, 40);
cout << "After the moving of pointsArray1:" << endl;
cout << "Point_0 of array2: " << pointsArray2.element(0).getX() << ", "
<< pointsArray2.element(0).getY() << endl;
cout << "Point_1 of array2: " << pointsArray2.element(1).getX() << ", "
<< pointsArray2.element(1).getY() << endl;
return 0;
}
在这个例子中,我们使用了复制构造函数来进行动态数组的复制。用图表示该过程:
复制之后,数组2与数组1并没有指向同一块内存空间,而是指向了不同的内存空间。可见,此时不仅仅是将数组一中的成员一一复制过来,而是将数组中指针指向的对象都复制过来了。如果我们不写复制构造函数,那么编译器会调用默认复制构造函数,进行浅层复制,那么就是将数组1中的指针复制过来,数组1和数组2占用的是同一块内存空间。在本例中,浅层复制可以用下图表示:
移动构造:
我们先来看一个案例,代码如下:
#include<iostream>
using namespace std;
class IntNum {
public:
IntNum(int x = 0) : xptr(new int(x)){ //构造函数
cout << "Calling constructor..." << endl;
}
IntNum(const IntNum & n) : xptr(new int(*n.xptr)){//复制构造函数
cout << "Calling copy constructor..." << endl;
};
~IntNum(){ //析构函数
delete xptr;
cout << "Destructing..." << endl;
}
int getInt() { return *xptr; }
private:
int *xptr;
};
//返回值为IntNum类对象
IntNum getNum() {
IntNum a;
return a;
}
int main() {
cout << getNum().getInt() << endl;
return 0;
}
运行结果:
在程序的执行过程中,先是构建了一个IntNum类临时对象,然后当需要将a返回到主调函数中时,函数调用了复制构造函数,将临时对象复制一份给a,然后返回a,并将该临时对象析构。观察这个过程,不难发现,我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制操作,也避免了多余的析构操作。如下图所示:
我们可以通过移动构造函数来实现这一操作:就是让这个临时对象它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。
复制构造和移动构造的差别:这种情况下,我们觉得这个临时对象完成了复制构造后,就不需要它了,我们就没有必要去首先产生一个副本,然后析构这个临时对象,这样费两遍事,又占用内存空间,索性将临时对象它的原本的资源直接转给构造的对象即可了。
当临时对象在被复制后,就不再被利用了。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制构造。那什么时候该触发移动构造呢?
如果临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候我们就可以触发移动构造。
移动构造函数定义形式:class_name(class_name && )
&&符号表示右值引用,右值是指即将消亡的值,函数返回的临时变量就是右值。
上述案例使用移动构造函数来实现:
#include<iostream>
using namespace std;
class IntNum {
public:
IntNum(int x = 0) : xptr(new int(x)){ //构造函数
cout << "Calling constructor..." << endl;
}
IntNum(const IntNum & n) : xptr(new int(*n.xptr)){//复制构造函数
cout << "Calling copy constructor..." << endl;
}
//使用即将消亡的对象n的指针来初始化指针,然后将n的指针置为空指针
IntNum(IntNum && n) : xptr(n.xptr){ //移动构造函数
n.xptr = nullptr;//
cout << "Calling move constructor..." << endl;
}
int getInt() { return *xptr; }
~IntNum(){ //析构函数
delete xptr;
cout << "Destructing..." << endl;
}
private:
int *xptr;
};
//返回值为IntNum类对象
IntNum getNum()
{
IntNum a;
return a;
}
int main() {
cout << getNum().getInt() << endl; return 0;
}
结果:
在本例中移动构造函数地代码为:
IntNum(IntNum && n) : xptr(n.xptr) //移动构造函数
{
n.xptr = nullptr;
cout << "Calling move constructor..." << endl;
}
看函数体里面,我们发现在做完xptr(n.xptr)这种指针对指针的复制(也就是把参数指针所指向的对象转给了当前正在被构造的指针)后,接着就把参数n里面的指针置为空指针(n.xptr = nullptr;),对象里面的指针置为空指针后,将来析构函数析构该指针(delete xpr;)时,是delete一个空指针,不发生任何事情,这就是一个移动构造函数。
移动构造函数中的参数类型,&&符号表示是右值引用;即将消亡的值就是右值,函数返回的临时变量也是右值,单个这样的引用可以绑定到左值的,而这个引用它可以绑定到即将消亡的对象,绑定到右值。
左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值指表达式结束时就不再存在的临时对象——显然右值不可以被取地址。
C++字符串
在C++中,并没有字符串变量,所以一般可以用字符数组来存储字符串,例如char str[8]=”program”,或者char str[8]={“p”,”r”,”o”,”g”,”r”,”a”,”m”,”