C++类和对象

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 深入析构函数

上述是栈对象在离开(超出)作用域被销毁时候,自动调用析构函数的例子,那么到底析构函数在哪些时候会被调用呢?
主要有以下几种场景:栈对象、全局静态对象、堆对象。

  1. 对于全局定义的对象,每当程序开始运行,在主函数main接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时自动调用全局对象的析构函数;
  2. 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数;
  3. 对于关键字static定义的静态局部变量,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时自动调用析构函数;
  4. 对于用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个步骤。

  1. 声明
类名 数组名[对象个数];

该格式会自动调用默认构造函数或参数都是缺省值的构造函数。

  1. 初始化
//完整书写数组元素个数和构造实参
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;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值