1. 面向对象初步认识
程序世界本质上只有两种东西:数据和逻辑。
数据天性喜静,构成了程序世界的本体和状态;
逻辑天性好动,作用于数据,推动程序世界的演进和发展。但是在数据和逻辑的存在形式和演进形式上,过程论和对象论的观点截然不同。
过程论认为:数据和逻辑是分离的、独立的,各自形成程序世界的一个方面(Aspect)。所谓世界的演变,是在逻辑作用下,数据做改变的一个过程。这种过程有明确的开始、结束、输入、输出,每个步骤有着严格的因果关系。过程是相对稳定的、明确的和预定义的,小过程组合成大过程,大过程还可以组合成更大的过程。所以,程序世界本质是过程,数据作为过程处理对象,逻辑作为过程的形式定义,世界就是各个过程不断进行的总体。
对象论认为:数据和逻辑不是分离的,而是相互依存的。相关的数据和逻辑形成个体,这些个体叫做对象,世界就是由一个个对象组成的。对象具有相对独立性,对外提供一定的服务。所谓世界的演进,是在某个“初始作用力”作用下,对象间通过相互调用而完成的交互;在没有初始作用力下,对象保持静止。这些交互并不是完全预定义的,不一定有严格的因果关系,对象间交互是“偶然的”,对象间联系是“暂时的”。世界就是由各色对象组成,然后在初始作用力下,对象间的交互完成了世界的演进。
过程论和对象论不是一种你死我活的绝对对立,而是一种辩证统一的对立,两者相互渗透、在一定情况下可以相互转化,是一种“你中有我、我中有你”的对立。如果将对象论中的所有交互提取出来而撇开对象,就变成了过程论,而如果对过程论中的数据和逻辑分类封装并建立交互关系,就变成了对象论。
过程论相对确定,有利于明晰演进的方向,但当事物过于庞大繁杂,将很难理清思路。因为过程繁多、过程中又有子过程,容易将整个世界看成一个纷繁交错的过程网,让人无法看清。
对象论相对不确定,但是因为以对象为基本元素,即使很庞大的事物,也可以很好地分离关注,在研究一个对象的交互时,只需要关系与其相关的少数几个对象,不用总是关注整个流程和世界,对象论更有助于分析规模较大的事物。但是,对象论也有困难。例如,如何划分对象才合理?对于同一个驱动力,为什么不同情况下参与对象和交互流程不一样?如何确定?其实,这些困难也正是面向对象技术中的困难。
面向过程语言:C语言,模块化,按照事物发展的逻辑顺序推进事物的发展。
面向对象的语言:C++语言,四大基本特征:抽象、封装、继承、多态。
详细了解请点击link
接下来,我们就开始讨论面向对象技术了。
1.1 类的产生
客观现实世界有的都是对象,万物皆对象,没有类。但大家都知道,在我们用Java、C++、C#等语言写代码时,都需要先定义一个类,之后再通过类创建对象,为什么如此呢?
从认识论来说,首先有具体认知能力,而后才能有抽象认知能力,抽象认知能力是一种高层的,人类特有的认知能力,它使我们可以从大量具体认知中,舍弃个别的、非本质的属性,提取出共同的、本质的属性,是形成概念的必要手段。所以从哲学角度说,是先有对象,然后才有类,类和对象是“一般和特殊”这一哲学原理在程序世界中的具体体现。
类可以帮助我们方便地认识和定义世界中的对象。这个作用是显而易见的。C++用类来描述对象,类是对现实世界中相似事物的抽象,比如同是“双轮车”的摩托车和自行车,有共同点,也有许多不同点。“车”类是对摩托车、自行车、汽车等相同点的提取与抽象。
1.2 类的定义
类的定义分为两个部分:数据(相当于属性)和对数据的操作(相当于行为)。
从程序设计的观点来说,类就是数据类型,是用户定义的数据类型,对象可以看成某个类的实例(某个类的变量)。所以说类是对象的封装,对象是类的实例。
C++中用关键字class来定义一个类,其基本形式如下:
class 类名
{
public://公有的,可以对外提供的
...//数据成员(属性、特征)
...//成员函数(行为、方法,操作属性的方法)
protected://保护的,留给儿子(子类)使用
...//数据成员(属性、特征)
...//成员函数(行为、方法,操作属性的方法)
private://私有的,留给自己使用
...//数据成员(属性、特征)
...//成员函数(行为、方法,操作属性的方法)
};
人:人的特征或者属性;交互的行为与方法。
举例Computer类如下:
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
//注意C++ 中的class能做的事儿,struct一样能做:
//1、将struct的功能做了提升,可以在其中定义函数,也可以设置权限;
//2、struct默认访问权限是public,而class默认访问权限是private.
class Computer
{
//左大括号到右大括号称为类的内部
public:
//三个方法、行为;成员函数
void setBrand(const char* brand)
{
//越界的风险,20字节
strcpy(_brand, brand);
}
void setPrice(float price)
{
_price = price;
}
void print()
{
cout << "_brand = " << _brand << endl;
cout << "_price = " << _price << endl;
}
private://封装性,不能在类的外面进行访问
//两个属性;数据成员
char _brand[20];//品牌
float _price;//价格
};//分号不要遗漏
int main(int argc, char* argv[])
{
//使用类Computer创建对象com
Computer com;
com.setBrand("ThinkPad");
com.setPrice(5300);
com.print();
/* com._price = 6000;//error,使用private封装性 */
return 0;
}
除了可以在类内部实现外,成员函数还可以在类之外实现。在类定义的外部定义成员函数时,应使用作用域限定符 :: 来标识函数所属的类,即有如下形式:
返回类型 类名::成员函数名(参数列表)
{
//....
}
我们在类之外实现成员函数,其代码如下:
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Computer
{
public:
void setBrand(const char* brand);
void setPrice(float price);
void print();
private:
char _brand[20];//品牌
float _price;//价格
};
void Computer::setBrand(const char* brand)
{
strcpy(_brand, brand);
}
void Computer::setPrice(float price)
{
_price = price;
}
void Computer::print()
{
cout << "_brand = " << _brand << endl;
cout << "_price = " << _price << endl;
}
int main(int argc, char* argv[])
{
//使用类Computer创建对象com
Computer com;
com.setBrand("ThinkPad");
com.setPrice(5300);
com.print();
return 0;
}
注意:写在一个文件或者写在类内中的成员函数,默认都是inline函数(内联函数),如果用分为头文件、实现文件、测试文件则该特征消失!
2. 对象的创建与销毁
2.1 构造函数
在上文的Computer类中,通过自定义的public成员函数setBrand()和setPrice()实现了对数据成员的初始化。实际上,C++为类提供了一种特殊的成员函数------构造函数来完成相同的工作。
构造函数在对象创建时自动调用,用以完成对象成员变量等的初始化及其他操作;如果程序员没有显式定义它,系统会提供一个默认构造函数。
下面我们用Point类来举例说明:
#include <iostream>
using std::cout;
using std::endl;
class Point
{
/*
构造函数有一些独特的地方:
-函数的名字与类名相同
-没有返回值
-没有返回类型,即使是void也不能有
*/
public:
/*
如果没有显示定义,编译器会自动提供一个默认(无参)构造函数:
Point()
{
}
*/
Point()
{
cout << "Point()" << endl;
}
};
int main(int argc, char* argv[])
{
Point pt;//创建对象会调用构造函数
return 0;
}
运行结果如下:
更深入构造函数:
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
//默认情况下,不写时,编译器会自动生成一个默认(无参)构造函数
Point()
: _ix(0)//初始化列表(初始化表达式)
, _iy(0)
{
/* _ix = 0;//赋值,不是初始化 */
/* _iy = 0; */
cout << "Point()" << endl;
}
//注意:一旦自定义了构造函数,那么编译器将不在提供默认(无参)构造函数,
//在这种情况下创建无参的对象就必须显示定义!
Point(int ix, int iy)
: _ix(ix)
, _iy(iy)
{
cout << "Point(int, int)" << endl;
}
void print()
{
cout << "(" << _ix
<< ", " << _iy
<< ")" << endl;
}
private:
int _ix;
int _iy;
};
int main(int argc, char* argv[])
{
Point pt;//创建对象会调用构造函数
cout << "pt = ";
pt.print();
cout << "=========" << endl;
Point pt1(3, 4);//创建对象pt1
cout << "pt2 = ";
pt1.print();
return 0;
}
运行结果如下:
2.1 初始化表达式
上述例子中,初始化列表位于构造函数形参列表之后,函数体之前,用冒号开始,如果有多个数据成员,再用逗号分隔,初始值放在一对小括号中。
显然,在构造函数初始化每个成员只能出现一次,令人意外的是,构造函数初始化列表只说明了初始化成员的值,而不限定初始化的具体执行顺序!即:成员初始化执行顺序与它们在类中声明的顺序一致,第一个成员先被初始化,然后第二个,依次类推。
一般情况下,初始化的顺序没什么特别要求,但一旦是用一个成员来初始化另一个成员,此时顺序就很关键啦!举例如下:
#include <iostream>
using std::cout;
using std::endl;
class Test
{
public:
Test(int value)
: _ix(value)
, _iy(_ix)
{
}
Test(double value1)
: _iy(value1) //在初始化列表中,_iy“仿佛”先被初始化
, _ix(_iy)
{
}
void print()
{
cout << "(" << _ix
<< ", " << _iy
<< ")" << endl;
}
private:
int _ix; //在声明时,_ix在前
int _iy;
};
int main(int argc, char* argv[])
{
Test tst(10);
cout << "tst = ";
tst.print();
Test tst1(0.333);
cout << "tst1 = ";
tst1.print();
return 0;
}
运行结果如下:
2.3 对象的销毁
构造函数在创建对象时被系统自动调用,而析构函数(Destructor)在对象被撤销时被自动调用,相比构造函数,析构函数要简单的多,用来执行一些清理任务,如释放成员函数中动态申请的内存等。如果程序员没有显式的定义它,系统也会提供一个默认的析构函数。
析构函数有如下特点:
- 与类同名,之前冠以波浪号**~**,以区别于构造函数。
- 析构函数没有返回类型,也不能指定参数。因此,析构函数只能有一个,不能被重载。
- 对象超出其作用域被销毁时,析构函数会被自动调用。
举例如下:
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
//默认情况下,不写时,编译器会自动生成一个默认(无参)构造函数
Point()
: _ix(0)//初始化列表(初始化表达式)
, _iy(0)
{
/* _ix = 0;//赋值,不是初始化 */
/* _iy = 0; */
cout << "Point()" << endl;
}
/*
注意:一旦自定义了构造函数,那么编译器将不在提供默认(无参)构造函数,
在这种情况下创建无参的对象就必须显示定义!
*/
Point(int ix, int iy)
: _ix(ix)
, _iy(iy)
{
cout << "Point(int, int)" << endl;
}
void print()
{
cout << "(" << _ix
<< ", " << _iy
<< ")" << endl;
}
/* 析构函数,默认情况下编译器会自动生成*/
~Point()
{
cout << "~Point()" << endl;
}
private:
int _ix;
int _iy;
};
void test()
{
/* int a;//栈变量 */
Point pt;//创建对象会调用构造函数,栈对象
/* pt.Point();//error,*/
cout << "1111111" << endl;
Point();//显示调用构造函数会创建对象,然后该对象会立马销毁
cout << "2222222" << endl;
cout << "3333333" << endl;
Point().print();
cout << "4444444" << endl;
cout << "pt = ";
pt.print();
pt.~Point();//析构函数是可以显示调用的,但是不建议显示调用
cout << "=========" << endl;
Point pt1(3, 4);//创建对象pt1
cout << "pt2 = ";
pt1.print();
pt1.~Point();
}
int main(int argc, char* argv[])
{
cout << "begin test..." << endl;
test();
cout << "finish test..." << endl;
return 0;
}
运行结果如下:
我们再举一个Computer例子:
//Computer.h
#ifndef __COMPUTER_H__
#define __COMPUTER_H__
class Computer
{
public:
Computer(const char* brand, float price);
void print();
~Computer();
private:
char *_brand;//品牌
float _price;//价格
};
#endif
//Computer.cc
#include "Computer.h"
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
Computer::Computer(const char* brand, float price)
: _brand(new char[strlen(brand)+1]())
, _price(price)
{
cout << "Computer(const char*, float)" << endl;
/* _brand = new char[strlen(brand)+1]()//申请空间,防止越界*/
strcpy(_brand,brand);
}
void Computer::print()
{
cout <<"(" << _brand
<< ", " << _price
<< ")" << endl;
}
//析构函数的清理操作有时候需要手动清理
Computer::~Computer()
{
cout << "~Computer()" << endl;
if(_brand)//处理内存泄漏
{
cout << "delete" << endl;
delete [] _brand;
_brand = nullptr;
}
}
//testComputer.cc
#include "Computer.h"
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char* argv[])
{
//使用类Computer创建对象com
Computer com("ThinkPad", 5300);
cout << "com = ";
com.print();
com.~Computer();//显示调用析构函数
com.print();
return 0;
}
运行结果如下:
2.4 深入析构函数
上述是栈对象在离开(超出)作用域被销毁时候,自动调用析构函数的例子,那么到底析构函数在哪些时候会被调用呢?
主要有以下几种场景:栈对象、全局静态对象、堆对象。
- 对于全局定义的对象,每当程序开始运行,在主函数main接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时自动调用全局对象的析构函数;
- 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数;
- 对于关键字static定义的静态局部变量,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时自动调用析构函数;
- 对于用new运算符创建的对象,每当创建该对象时调用构造函数,当用delete删除该对象时,调用析构函数(即需手动执行delete操作)。
//Computer.h
#ifndef __COMPUTER_H__
#define __COMPUTER_H__
class Computer
{
public:
Computer(const char* brand, float price);
void print();
~Computer();
private:
char *_brand;//品牌
float _price;//价格
};
#endif
//Computer.cc
#include "Computer.h"
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
Computer::Computer(const char* brand, float price)
: _brand(new char[strlen(brand)+1]())
, _price(price)
{
cout << "Computer(const char*, float)" << endl;
/* _brand = new char[strlen(brand)+1]()//申请空间,防止越界*/
strcpy(_brand,brand);
}
void Computer::print()
{
cout <<"(" << _brand
<< ", " << _price
<< ")" << endl;
}
//析构函数的清理操作有时候需要手动清理
Computer::~Computer()
{
cout << "~Computer()" << endl;
if(_brand)//处理内存泄漏
{
cout << "delete" << endl;
delete [] _brand;
_brand = nullptr;
}
}
//testComputer.cc
#include "Computer.h"
#include <iostream>
using std::cout;
using std::endl;
Computer gComputer("huawei", 8000);//全局对象
void test()
{
//使用类Computer创建对象com
Computer com("ThinkPad", 5300);//栈对象
cout << "com = ";
com.print();
/* com.~Computer();//显示调用析构函数 */
/* com.print(); */
Computer *pc = new Computer("xiaomi", 6000);//堆对象
pc->print();
delete pc;//堆对象需要显示的使用delete,否则析构函数调用不到
pc = nullptr;
}
int main(int argc, char* argv[])
{
gComputer.print();
cout << "begin main..." << endl;
test();
cout << "finish main..." << endl;
return 0;
}
运行结果如下:
3. 拷贝构造函数
3.1 基本形式
C++中经常会使用一个变量初始化另一个变量,如:
int x = 1;
int y = x;
我们希望这样的操作也能作用于自定义类类型,如:
Point pt1(1, 2);
Point pt2 = pt1;
这两组操作是不是一致的呢?
第一组好说,而第二组只是将类型换成了Point类型,执行Point pt2 = pt1;语句时,pt1对象已经存在,而pt2对象还不存在,所以也是这句创建了pt2对象,既然涉及到对象的创建,就必然需要调用构造函数,而这里会调用的就是复制构造函数,又称为拷贝构造函数。当我们进行测试时,会发现我们不需要显式给出拷贝构造函数,就可以执行第二组测试。这是因为如果类中没有显式定义拷贝构造函数时,编译器会自动提供一个缺省(默认)的拷贝构造函数。其原型是:
类名::类名(const 类名 &);
例如:
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
Point(int ix = 0, int iy = 0)
: _ix(ix)
, _iy(iy)
{
cout << "Point(int, int)" << endl;
}
//编译器会自动生成拷贝构造函数
Point(const Point &rhs)
: _ix(rhs._ix)
, _iy(rhs._iy)
{
cout << "Point(const Point &)" << endl;
}
void print()
{
cout << "(" << _ix
<< ", " << _iy
<< ")" << endl;
}
private:
int _ix;
int _iy;
};
void test()
{
/* int a;//栈变量 */
Point pt1;//创建对象会调用构造函数,栈对象
cout << "pt1 = ";
pt1.print();
cout << "=========" << endl;
Point pt2(3, 4);//创建对象pt2
cout << "pt2 = ";
pt2.print();
}
void test1()
{
/*
int a = 10;
int b = a;
cout << "a = " << a << endl
<< "b = " << b << endl;
*/
Point pt3(1,2);
cout << "pt3 = ";
pt3.print();
Point pt4 = pt3;
cout << "pt4 = ";
pt4.print();
}
int main(int argc, char* argv[])
{
test();
cout << "=========" << endl;
test1();
return 0;
}
运行结果如下:
3.2 浅拷贝与深拷贝
#if 0
//编译器默认生成的拷贝构造函数
Computer::Computer(const Computer &rhs)
//浅拷贝,指针指向同一块堆空间的字符串
//销毁或改变时,会影响另一对象
: _brand(rhs._brand)
, _price(rhs._price)
{
cout << "Computer(const Computer &)" << endl;
}
#endif
//自定义拷贝构造函数
Computer::Computer(const Computer &rhs)
//先申请空间,深拷贝两个对象都拥有各自的独立堆空间字符串,
//一个对象销毁时就不会影响另一个对象。
: _brand(new char[strlen(rhs._brand)+1]())
, _price(rhs._price)
{
cout << "Computer(const Computer &)" << endl;
strcpy(_brand,rhs._brand);//再拷贝
}
//执行构造初始化
Computer com1("ThinkPad", 5300);
Computer com2 = com1;
3.3 三种调用时机
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
Point(int ix = 0, int iy = 0)
: _ix(ix)
, _iy(iy)
{
cout << "Point(int, int)" << endl;
}
//编译器会自动生成拷贝构造函数
/*
思考两个问题:
Q1:拷贝构造函数参数中的引用符号能不能去掉?
Q2:拷贝构造函数参数中的const关键字能不能去掉?
*/
Point(const Point &rhs)
: _ix(rhs._ix)
, _iy(rhs._iy)
{
cout << "Point(const Point &)" << endl;
}
void print()
{
cout << "(" << _ix
<< ", " << _iy
<< ")" << endl;
}
~Point()
{
cout << "~Point()" << endl;
}
private:
int _ix;
int _iy;
};
void test()
{
/* int a;//栈变量 */
Point pt1;//创建对象会调用构造函数,栈对象
cout << "pt1 = ";
pt1.print();
cout << "=========" << endl;
Point pt2(3, 4);//创建对象pt2
cout << "pt2 = ";
pt2.print();
}
Point func1()
{
Point pt5(5,6);
cout << "pt5 = ";
pt5.print();
return pt5;//将pt5值拷贝出去
}
/*类比:
void func(int a) // int a = b;
{
}
func(b);
*/
/*拷贝构造函数的调用时机2:
当函数的参数是类类型的时候,在进行形参与实参结合的时候,会调用拷贝构造函数
*/
void func(Point pt) //Point pt = pt4;转换为调用时机1
{
cout << "pt = ";
pt.print();
}
void test1()
{
/*
int a = 10;
int b = a;
cout << "a = " << a << endl
<< "b = " << b << endl;
*/
Point pt3(1,2);
cout << "pt3 = ";
pt3.print();
/*拷贝构造函数调用时机1:
当用一个已经存在的对象去初始化一个刚刚创建的对象,会调用拷贝构造函数
*/
Point pt4 = pt3;
cout << "pt4 = ";
pt4.print();
func(pt4);
Point pt6 = func1();
cout << "pt6 = ";
pt6.print();
}
int main(int argc, char* argv[])
{
test();
cout << "=========" << endl;
test1();
return 0;
}
运行结果如下:
3.4 参数问题
上面例子中遗留两个思考问题,现分析如下:
- Q1:拷贝构造函数参数中的引用符号能不能去掉?
- A1:引用符号不能去掉。因为如果去掉引用符号,那么在进行形参与实参结合的时候,会调用拷贝构造函数,而拷贝构造函数现在没有引用符号,那么就是用一个已经存在的对象初始化一个刚刚创建的对象,即满足拷贝构造函数的调用时机1,会继续无限的调用拷贝构造函数,由于函数的参数是会入栈的,而栈的大小是有大小的,这样就将栈压垮,导致栈溢出。
- Q2:拷贝构造函数参数中的const关键字能不能去掉?
左值:可以进行取地址的;
右值:不能进行取地址的,右值包括:临时对象、匿名对象、临时变量、匿名变量、字面值常量(10)等
-A2:不能去掉,当执行拷贝构造函数的时候,如果传递进来的参数是右值的时候,会出现 非const左值引用不能绑定到右值,就会出现错误。(注意:没有const的时候,如果传递的参数是左值是没有问题的)const解决左值、右值问题!
4. 隐藏的this指针
在上面的例子中,我们通过对象名调用成员函数后,都能准确的访问相应对象的数据成员,而不会出错,这到底是如何实现的呢?比如:
Point pt1(1, 2);
pt1.print(); //(1, 2)
Point pt2(2, 3);
pt2.print(); //(2, 3)
实际上,在类中定义的非静态成员函数中都存在一个隐藏的this指针,用来记录对象本身的地址,代表的就是当前对象本身,位于成员函数的第一个参数,由编译器自动补全。函数体内所有对类数据成员的访问, 都会被转化为this->数据成员的方式,例如print()的完整实现是:
void Point::print(Point *const this)
{
cout << "(" << this->_ix
<< ", " << this->_iy
<< ")" << endl;
}
特点:指针常量!
this->_ix = 10;//ok,内容可改
this = nullptr;//error,指向不变
this = 0x7fff3456;//error
5. 赋值运算符函数
5.1 基本形式
类比内置类型我们将赋值操作作用于自定义类类型,比如:
int a = 10;
int b = 20;
b = a;//赋值,赋值运算符左侧运算对象必须是一个可修改的左值
Point pt1(1,2);
Point pt2(2,3);
pt2 = pt1;//赋值,pt1与pt2都存在,不存在对象的构造
/* 原本形式: pt2.operator=(pt1); */
在这里,当=作用于对象时,其实是把它当成一个函数来看待的。在执行pt1 = pt2;该语句时,需要调用的是赋值运算符函数。其形式如下:
返回类型 类名::operator=(参数列表)
{
//...
}
对于Point类而言,其实现如下:
//默认情况下,如果类中无显示定义,编译器会自动生成赋值运算符函数
Point &operator=(const Point &rhs)
{
cout << "Point &operator=(const Point &)" << endl;
this->_ix = rhs._ix;
_iy = rhs._iy;
return *this;
}
5.2 重写赋值运算符函数
当我们对Computer对象也执行赋值操作时,又会发生什么呢?先看Computer类的默认赋值运算符函数的实现:
Computer &Computer::operator=(const Computer &rhs)
{
_brand = rhs._brand;//浅拷贝
_price = rhs._price;
return *this;
}
很显然,这里默认情况下编译器提供浅拷贝的方式仅仅拷贝堆空间地址,不仅存在内存泄漏,而且当一个对象改变或被销毁时,另外一个对象受到影响和存在double free的风险。
因此,我们显示定义赋值运算符函数,总结起来有四步:
1、防止自复制;2、释放左操作数;
3、进行深拷贝;4、返回*this
实现如下:
Computer &Computer::operator=(const Computer &rhs)
{
cout << "Computer &operator=(const Computer &)" << endl;
/*if(不是自己赋值给自己)*/
if(this != &rhs)//1.防止自复制
{
delete [] _brand;//2.释放左操作数,解决内存泄漏
_brand = nullptr;
//3.深拷贝,防止内存越界
_brand = new char[strlen(rhs._brand)+1]();
strcpy(_brand,rhs._brand);
_price = rhs._price;
}
return *this;//4.返回*this
}
5.3 参数与返回类型
- Q1:赋值运算符函数参数中的引用符号能不能去掉?
- A1:不能去掉。否则在形参与实参结合的时候会调用拷贝构造函数,多执行一次函数调用,就损失了效率。(注意:不存在栈溢出)
- Q2:赋值运算符函数参数中的关键字const能不能去掉?
- A2:对于右值传递会有问题。如果传递进来的参数是右值的时候,会出现 非const左值引用不能绑定到右值,与之前研究的拷贝构造函数参数中的const不能去掉的原因一致。
- Q3:赋值运算符函数的返回类型中的引用能不能去掉?
- A3:不能去掉。一旦去掉,那么赋值运算符函数的返回类型是一个类类型,满足拷贝构造函数调用时机3,在执行return语句时候,会执行拷贝构造函数,就损失了效率。
- Q4:赋值运算符函数的返回类型可以是空吗?
- A4:不能。会失去“连等”,即语句pt3 = pt2 = pt1;无法执行。
6. 特殊数据成员的初始化
在C++的类中,有4种比较特殊的数据成员,他们分别是常量成员、引用成员、类对象成员和静态成员,他们的初始化与普通数据成员有所不同。
6.1 常量数据成员
6.2 引用数据成员
和常量成员相同,引用成员也必须在构造函数初始化列表中进行初始化,否则编译报错。
class Point
{
public:
//错误写法
Point(int ix = 0, int iy = 0)
{
_ix = 3;//error,赋值
_iy = 4;//const数据成员“只读属性”
_ref = ix;//error,赋值
}
//正确写法
Point(int ix = 0, int iy = 0)
: _ix(ix)//初始化列表才是真正初始化的位置
, _iy(iy)
, _ref(ix)//引用数据成员也必须在初始化列表进行初始化
{
}
private:
const int _ix;//常量数据成员,必须在初始化列表进行初始化
const int _iy;
int & _ref;//引用占据一个指针大小的空间
};
6.3 类对象数据成员
当自定义类类型对象作为另一类类型的数据成员时,比如一个直线类Line对象中包含两个Point类对象,对Point对象的创建就必须要放在Line的构造函数的初始化列表中进行。
class Line
{
public:
Line(int x1, int y1, int x2, int y2)
: _pt1(x1, y1)//类对象数据成员放在初始化列表中进行初始化
, _pt2(x2, y2)
{
cout << "Line(int, int, int, int)" << endl;
}
void printLine()
{
_pt1.print();
cout << "---->";
_pt2.print();
cout << endl;
}
~Line()
{
cout << "~Line()" << endl;
}
private:
Point _pt1;//类对象数据成员,或称子对象
Point _pt2;
};
当Line的构造函数没有在其初始化列表中初始化对象_pt1和_pt2时,系统也会自动调用Point类的默认构造函数,此时就会与预期的构造不一致。因此需要显式在Line的构造函数初始化列表中初始化_pt1和_pt2对象。
6.4 静态数据成员
C++允许使用static(静态存储)修饰数据成员,这样的成员在编译时就被创建并初始化的(与之相比,对象是在运行时被创建的),且其实例只有一个,被所有该类的对象共享,不受访问权限的控制,就像住在同一宿舍里的同学共享一个房间号一样。静态数据成员和之前介绍的静态变量一样,当程序执行时,该成员已经存在,一直到程序结束,任何该类对象都可对其进行访问,静态数据成员存储在全局/静态区,并不占据对象的存储空间
以Compute为例,模拟购买电脑的过程,为了获取总价,我们定义了一个静态变量_totalPrice:
class Computer
{
public:
Computer(const char *brand, double price)
: _brand(new char[strlen(brand) + 1]())
, _price(price)
{
_totalPrice += _price;
}
void print()
{
cout << "品牌:" << _brand << endl
<< "价格:" << _price << endl
<< "总价:" << _totalPrice << endl;
}
//...
private:
char * _brand;
double _price;
static double _totalPrice;
};
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类对象时被定义的。这意味着它们不是由类的的构造函数初始化的,一般来说,我们不能在类的内部初始化静态数据成员,必须在类的外部定义和初始化静态数据成员,且不再包含static关键字,格式如下:
类型 类名::变量名 = 初始化表达式; //普通变量
类型 类名::对象名(构造参数); //对象变量
Computer中的静态变量_totalPrice的初始化如下:
double Computer::_totalPrice = 0;
7. 特殊的成员函数
7.1 静态成员函数
7.2 const成员函数
以Computer类为例,模拟购买电脑的过程,来说明者两个特殊的成员函数的特点,代码如下:
//Computer.h
#ifndef __COMPUTER_H__//防止该文件在另外某个文件中被包含多次
#define __COMPUTER_H__
class Computer
{
public:
Computer(const char* brand, float price);
Computer &operator=(const Computer &rhs);
//两个print可以重载,原因是隐藏的this不一样
void print();
void print() const;
static void printTotalPrice();
~Computer();
private:
char *_brand;//品牌
float _price;//价格
static float _totalPrice;//总价
};
#endif
//Computer.cc
#include "Computer.h"
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
float Computer::_totalPrice = 0;//总价
Computer::Computer(const char* brand, float price)
: _brand(new char[strlen(brand)+1]())
, _price(price)
/* , _totalPrice(0) //error */
{
cout << "Computer(const char*, float)" << endl;
/* _brand = new char[strlen(brand)+1]()//申请空间,防止越界*/
strcpy(_brand,brand);
_totalPrice += _price;
}
Computer &Computer::operator=(const Computer &rhs)
{
cout << "Computer &operator=(const Computer &)" << endl;
/*if(不是自己赋值给自己)*/
if(this != &rhs)//1.防止自复制问题
{
delete [] _brand;//2.释放左操作数,解决内存泄漏
_brand = nullptr;
/*this-> _brand = rhs._brand;//浅拷贝 */
//3.深拷贝
_brand = new char[strlen(rhs._brand)+1]();//防止内存越界
strcpy(_brand,rhs._brand);
_price = rhs._price;
}
return *this;//4.返回*this
}
void Computer::print(/* Computer * const this */)
{
cout << "void print()" << endl;
/* this = nullptr; //error */
this->_price = 6666; //ok!
cout <<"(" << _brand
<< ", " << _price
<< ")" << endl;
}
/*
const成员函数的特点:
1.具有只读特性(对数据成员只能读不能改);
2.非const的对象默认情况调用的是非const版本的成员函数;而const对象调用
的是const版本的成员函数,非const对象也是可以调用const版本的成员函数;
3.const对象是不能调用非const版本的成员函数;
4.同时存在两个版本,建议先写const版本的成员函数。
*/
void Computer::print(/* const Computer * const this */) const
{
cout << "void print() const" << endl;
/* this = nullptr; //error */
/* this->_price = 6000; //error! */
cout <<"(" << _brand
<< ", " << _price
<< ")" << endl;
}
/*
静态成员函数的特点:
1.其第一个参数的位置没有隐藏的this指针;
2.静态的成员函数不能访问非静态的数据成员和非静态的成员函数;
3.非静态的成员函数不能访问静态的数据成员和静态的成员函数;
4.如果静态成员函数想访问非静态的数据成员或成员函数,可以使用
函数传参数的形式,或在静态成员函数体中创建对象;
5.静态成员函数可以使用类名与作用域限定符的形式进行调用
(独特之处,其他的非静态成员函数不能这么调用)。
*/
void Computer::printTotalPrice()
{
/* Computer com("sdcs", 4545); */
/* com.print(); */
/* printf("this: %p\n", this);//error,没有this指针 */
/* _price = 5000; */
/* this->print(); */
cout << "_totalPrice = " << _totalPrice << endl;
}
//析构函数的清理操作有时候需要手动清理
Computer::~Computer()
{
cout << "~Computer()" << endl;
_totalPrice -= _price;
if(_brand)//处理内存泄漏
{
cout << "delete" << endl;
delete [] _brand;
_brand = nullptr;
}
}
//testComputer.cc
#include "Computer.h"
#include <iostream>
using std::cout;
using std::endl;
void test()
{
cout << "购买电脑之前,总价为:";
Computer::printTotalPrice();
Computer com1("ThinkPad", 5300);
cout << "com1 = ";
com1.print();
/* Computer::print(); error! */
cout << "购买第一台电脑的总价:";
com1.printTotalPrice();//或Computer::printTotalPrice(); ok!
Computer com2("Huawei", 8000);
cout << "com2 = ";
com2.print();
cout << "购买第二台电脑的总价:";
com2.printTotalPrice();//或Computer::printTotalPrice();
}
void test1()
{
const Computer com3("xiaomi", 6000);
cout << "com3 = ";
com3.print();
Computer com4("oppo", 5500);
cout << "com4 = ";
com4.print();
}
int main(int argc, char* argv[])
{
test();
test1();
return 0;
}
运行结果如下:
8. 对象的组织
有了自己定义的类,或者使用别人定义好的类创建对象,其机制与使用int等内置类型创建普通变量几乎完全一致,可以创建const对象、指向对象的指针、对象数组以及堆对象。
8.1 const对象
类对象也可以声明为const对象,因为const对象只能被创建、撤销以及只读访问,改写是不允许的,因此除了构造函数和析构函数,能作用于const对象只有const成员函数,如:
const Point pt(1, 2);
8.2 指向对象的指针
和普通变量一致,对象占据一定的内存空间,指针中存储的是对象所占内存空间的首地址。 C++ 程序中采用如下形式声明指向对象的指针:
类名 *指针名 [=初始化表达式];
初始化表达式是可选的,下列形式都是合法的:
/*调用构造函数*/
Point pt1;
Point pt(1, 2);
/*不调用构造函数*/
Point *ppt1 = nullptr;//1.直接置为nullptr,在程序中对该指针赋值
Point *ppt2 = &pt;//2.可以通过取地址(&对象名)给指针初始化
Point *ppt3 = new Point(3, 4);//3.可以通过申请动态内存给指针初始化
Point *ppt4 = new Point[5]();//堆空间
ppt3->print();//指针直接通过指向访问成员函数,合法
(*ppt3).print();//指针通过解引用由对象调用成员函数,合法
8.3 对象数组
对象数组和标准类型数组的使用方法并没有什么不同,也有声明、初始化和使用3个步骤。
- 声明
类名 数组名[对象个数];
该格式会自动调用默认构造函数或参数都是缺省值的构造函数。
- 初始化
//完整书写数组元素个数和构造实参
Point pt1(7, 8);
Point pt[4] = {Point(1, 2), Point(3, 4), Point(5, 6), pt1};
//缺省数组元素个数
Point pt[] = {Point(1, 2), Point(3, 4)};
//未将数组所有元素进行初始化
Point pt[4] = {Point(1, 2), Point(3, 4)};
//或者,去掉Point的书写,将( )换成{ }
Point pt[4] = {{1, 2}, {3, 4}, {5, 6}, pt1};
Point pt[] = {{1, 2}, {3, 4}};
Point pt[4] = {{1, 2}, {3, 4}};
3.对象数组元素的访问
// 数组名[下标].成员函数
pt[0].print();
8.4 堆对象
和把一个简单变量创建在动态存储区一样,可以用new和delete表达式为对象分配动态存储区,这里主要讨论如何为对象和对象数组动态分配内存:
void test()
{
Point *pt1 = new Point(11, 12);
pt1->print();
delete pt1;//new与delete成对出现
pt1 = nullptr;
/*注意:
使用new表达式为对象数组分配动态空间时,不能显式调用对象的构造函数,
因此,对象要么没有定义任何形式的构造函数(由编译器缺省提供),
要么显式定义了一个(且只能有一个)所有参数都有缺省值的构造函数!
*/
Point * pt2 = new Point[5]();
//赋值方式:
pt2[0] = Point(1, 2); //方法1
pt2[1] = {3, 4}; //方法2
//访问元素:
pt2->print();
(pt2 + 1)->print(); //方法1
pt2[4].print(); //方法2
delete [] pt2;
}