第二章 类和对象

第二章 类和对象

一、C++中的类

1.类的定义

C++中用关键字class来定义一个类,其基本形式如下:

class 类名 
{ 
public:     //公有数据成员和成员函数 
protected:  //保护数据成员和成员函数 
private:    //私有数据成员和成员函数 
};  // 千万不要忘了这个分号

public进行修饰的成员表示的是该类可以提供的接口、功能、或者服务;

protected进行修饰的成员,其访问权限是开放给其子类;

private进行修饰的成员是不可以在类之外进行访问的只能在类内部访问,可以说封装性就是由该关键字来体现。

在类中定义的成员函数,都是 inline 函数。除了可以在类内部实现外,成员函数还可以在类之外实现。在类定义的外部定义成员函数时,应使用作用域限定符(::)来标识函数所属的类,即有如下形式:

返回类型 类名::成员函数名(参数列表) 
{    
    //.... 
}
//类的实现:成员函数的实现
class Computer//类的内部:大括号以内
{ 
public://对外的接口(服务)
    //成员函数
    //默认情况,在类内部实现的成员函数是内联函数
    inline
    void setBrand(const char* brand)
    {
        strcpy(_brand,brand);
    }
    inline
    void setPrice(float price)
    {
        _price=price;
    }
    inline
    void print()
    {
        cout << "brand : " << _brand << endl;
        	 << "price : " << _price << endl;
    }
private: //封装,不能再类的外面使用
    //成员数据
    char _brand[20];
    float _price;
};
int main()
{
    Computer pc;
    pc.setBrand("Lenovo");
    pc.setPrice(6900);
    pc.print();
    
    strcpy(pc._brand,"Mac"); //(❌)error private只能类内部访问
    pc._price=15000; //(❌)error private只能类内部访问
    pc.print();
    return 0;
}

2.classstruct的区别

//C++对struct关键字做了提升,与class类似(里面可以实现函数)
//struct成员函数,访问权限是public;class成员函数,访问权限是private
struct Computer
{ 
    void setBrand(const char* brand)
    {
        strcpy(_brand,brand);
    }
    void setPrice(float price)
    {
        _price=price;
    }
    void print()
    {
        cout << "brand : " << _brand << endl;
        	 << "price : " << _price << endl;
    }
    char _brand[20];
    float _price;
}

二、对象的创建与销毁

1.对象的创建(注意初始化)

构造函数创建对象时被系统自动调用,C++ 为类提供了一种特殊的成员函数----构造函数来对数据成员进行初始化。构造函数有一些独特的地方:

  • 函数的名字与类名相同
  • 没有返回值
  • 没有返回类型,即使是 void 也不能有
class Point 
{
public:
    //默认情况(当我们没有自定义构造函数),编译器也会自动提供一个空函数
    //Point()
    //{       
    //}
    
    //构造函数是支持重载的
    Point()     
    : _ix(0)   //初始化表达式(列表),它才是对数据成员进行真正的初始化
    , _iy(0)   //初始化列表中的数据成员的初始化,只与数据成员声明的时候的顺序有关
    {          //不能先初始化_iy,再初始化_ix
        cout << "Point()" << endl;
        _ix = 0;  //赋值
        _ix = 0;
    }
    
    //构造函数的作用:为了初始化数据成员
    Point(int ix, int iy)
    : _ix(ix)  //初始化
    , _iy(iy)
    {
        cout << "Point(int , int)" << endl;
        _ix = ix;  //赋值
        _iy = iy;
    }
    void print()
    {
        cout << "ix : " << _ix << endl;
             << "iy : " << _iy << endl;
    }
private:
    int _ix;
    int _iy;
}

int main()
{
    //Point pt;  当我们有自定义的构造函数时,系统提供的构造函数就不能使用了
    Point pt(1,2); //对象在创建的时候,调用了构造函数
    pt.print();
    return 0;
}

2.对象的销毁

析构函数(Destructor)对象被撤销时被自动调用,析构函数有如下特点:

  • 与类同名,之前冠以波浪号,以区别于构造函数。
  • 析构函数没有返回类型,也不能指定参数。因此,析构函数只能有一个,不能被重载。
  • 对象超出其作用域被销毁时,析构函数会被自动调用。
class Point 
{ 
public:    
    //...    
    
    //析构函数:没有函数返回值,没有参数,不能重载,具有唯一性
    //默认情况,编译器自动生成
    //析构函数在main函数中可以显示调用,但是不建议这么做
    ~Point() 
    {
       
    }    
    //... 
};

析构函数清理资源-实例:

class Computer 
{ 
public:    
    Computer(const char * brand, double price)   
    //一个指针直接赋值给另一个指针,这两个指针指向同一位置,一个修改另一个也跟着变化
    : _brand(new char[strlen(brand) + 1]())   //需要先申请空间,再拷贝数据
    , _price(price)                         
    {
        strcpy(_brand, brand)
    }        
    
    ~Computer()    //析构函数可以显示调用,但是不要这么操作
    {        
        if(_brand)
        {
            delete [] _brand;  //清理对象中成员的资源   
            _brand = nullptr; //C++中尽量不用NULL
        }
        cout << "~Computer()" << endl;    
    }   
private:    
    char * _brand;    
    double _price; 
};

析构函数调用时机:

  • 对于全局定义的对象,每当程序开始运行,在主函数 main 接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时,自动调用全局对象的析构函数。
  • 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
  • 对于关键字 static 定义的静态局部对象,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
  • 对于用 new 运算符创建的对象,每当创建该对象时调用构造函数,当用 delete 删除该对象时,调用析构函数。

三、拷贝构造函数

1.深拷贝、浅拷贝

//原型:
类名::类名(const 类名&);

Point::Point(const Point & rhs) 
: _ix(rhs._ix) 
, _iy(rhs._iy) 
{
    
}

实例:

//Q:拷贝构造函数的引用为什么不能去掉?
//A:如果引用去掉,拷贝过程中会无穷递归下去,并且没有出口
//Q:拷贝构造函数的const为什么不能去掉?
//A:如果const去掉,当传递对象(右值)的时候,会报错 
    //const Point &rhs = func3();    //ok
    //Point &rhs = func3();       //error(❌)
Computer::Computer(const Computer & rhs) 
: _brand(new char[strlen(rhs._brand) + 1]()) //深拷贝,两个对象有各自独立的堆空间字符串(new char[]())
, _price(rhs._price) 
{    
    strcpy(_brand, rhs._brand); 
}

Computer::Computer(const Computer & rhs) 
: _brand(rhs._brand)   //浅拷贝,两个对象指向同一个堆空间的字符串
, _price(rhs._price) 
{
    
}


//执行构造初始化 
Computer pc1("Huawei Matebook14", 5699); 
Computer pc2 = pc1;

拷贝构造函数的调用时机:

  • 当把一个已经存在的对象赋值给另一个新对象时,会调用拷贝构造函数。
  • 当实参和形参都是对象,进行实参与形参的结合时,会调用拷贝构造函数。
  • 当函数的返回值是对象,函数调用完成返回时,会调用拷贝构造函数。

附:左值、右值

左值:可以取地址

右值:不可以取地址(临时对象/匿名对象func4() 字面值常量10),因为其生命周期只在本行,不是一个持久化的量(对象),在内存里面没有存起来,所以不能取地址

四、隐含的this指针

在类中定义的非静态成员函数中都有一个隐含的this指针,它代表的就是当前对象本身,它作为成 员函数的第一个参数,由编译器自动补全.

//实质:指向对象本身
//存在位置:隐含在成员函数的第一个参数处
void print( /*  Point * const this  */)
{
    // this =nullptr;   (❌)error,this是常量指针
    this->ix = 0;
    cout << "(" << this->ix
         << "," << this->iy
         << ")" <<endl;
}
void test
{
    Point pt1(1, 5);
	pt1.print(); //pt1.print(&pt1); 把对象的地址传入
}

五、赋值运算符函数

Point pt1(1,2),pt2(3,4);
//在执行pt1=pt2;该语句时,pt1与pt2都存在,所以不存在对象的构造,这要与Point pt2=pt1;
pt1 = pt2;  //对象赋值操作(两个已经定义了的对象),需要赋值运算符函数
		    
//实质形式:
返回类型 类名::operator=(参数列表)
{
    //......
}
//没有显式定义赋值运算符函数时,编译器会自动提供一个缺省的赋值运算符函数
//Q:赋值运算符函数返回值的引用能否去掉?
//A:不能去掉,否则在执行赋值操作的时候,会满足拷贝构造函数的调用时机3,会执行拷贝构造函数,效率就降低了,开销大了
//Q:赋值运算符函数返回值类型能不能是void?
//A:不能返回void,否则不能执行连等操作 pt1 = pt2 = pt3
Computer & Computer::operator=(const Computer & rhs) 
{    
    _brand = rhs._brand;   
    _price = rhs._price;   
    return *this; //若返回类型为A, 则是克隆, 若返回类型为A&,则是本身 
    			  //return this返回当前对象的地址(指向当前对象的指针)
}

赋值运算符函数的四部曲:

Computer & Computer::operator=(const Computer & rhs) 
{       
    //赋值运算符函数:四部曲
    if(this != &rhs)  //1.自复制的情况去除
    {        
        delete [] _brand;   //2.释放左操作数原先的堆空间         
        _brand = new char[strlen(rhs._brand) + 1]();  //3.执行深拷贝   
        strcpy(_brand, rhs._brand);                
        _price = rhs._price;    
    }    
    return *this;  //4.返回*this
}

六、特殊数据成员的初始化

C++的类中,有4种比较特殊的数据成员,他们分别是常量成员、引用成员、类对象成员和静态成员,他们的初始化与普通数据成员有所不同。

1.常量数据成员

当数据成员用const关键字进行修饰以后,就成为常量成员。一经初始化,该数据成员便具有“只读属性”,在程序中无法对其值修改。事实上,在构造函数体内初始化 const 数据成员是非法的,它们只能在构造函数初始化列表中进行初始化。如:

class Point { 
public:    
    //错误写法    
    Point(int ix = 0, int iy = 0)    
    {        
        _ix = ix; //(❌)error, 这是赋值语句,const成员不能修改        
        _iy = iy; //(❌)error, 这是赋值语句,const成员不能修改      
        _iz = _ix;    
    }        
    
    //正确写法    
    Point(int ix = 0, int iy = 0)    
    : _ix(ix)   
    , _iy(iy)    
    , _iz(_ix) // int & _iz = _ix (引用初始化)
    //, _iz(0)   (❌)error, int & _iz = 0; 语法错误
    {
        
    }     
private:    
    const int _ix;    
    const int _iy;    
    int & _iz;   //引用数据成员,需要在初始化表达式中进行初始化
    			 //引用作为数据成员的时候,占用一个指针大小的空间
};

2.引用数据成员

见上:

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;    
    } 
private:    
    Point _pt1;    
    Point _pt2; 
};

Line的构造函数没有在其初始化列表中初始化对象_pt1_pt2时,系统也会自动调用 Point类的默认构造函数,此时就会与预期的构造不一致。因此需要显式在Line的构造函数初始化列表中初始化_pt1_pt2对象。

4.静态数据成员

C++允许使用static(静态存储)修饰数据成员,这样的成员在编译时就被创建并初始化的(与之相比,对象是在运行时被创建的),且其实例只有一个,被所有该类的对象共享

class Computer { 
public:    
    Computer(const char * brand, double price)    
    : _brand(new char[strcpy(brand) + 1]())    
    , _price(price)
    //, _totalpriceK(0)  (❌)error,静态数据成员不能在类内初始化
    {        
        _totalPrice += _price;    
    }        
    
    void print()    
    {        
        cout << "品牌:" << _brand << endl             
             << "价格:" << _price << endl             
             << "总价:" << _totalPrice << endl;    
    }    
    //... 
private:    
    char * _brand;    
    double _price;    //内存对齐 换成float之后占用的内存大小是一样的
    static double _totalPrice; //静态数据成员不占用类的空间,所有的对象所共享的
    						   //必须在类外面进行初始化
};

//静态数据成员必须放到类外部进行初始化(特别是,对于头文件与实现文件这种形式:必须放在实现文件中)
double Computer::_totalprice = 0;

因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类对象时被定义的。这意味着它们不是由类的的构造函数初始化的,一般来说,我们不能在类的内部初始化静态数据成员,必须在类的外部定义和初始化静态数据成员,且不再包含static关键字,格式如下:

类型 类名::变量名 = 初始化表达式; //普通变量 
类型 类名::对象名(构造参数);     //对象变量

七、特殊的成员函数

除了特殊的数据成员以外, C++类中还有两种特殊的成员函数:静态成员函数和 const 成员函数。

1.静态成员函数

静态成员函数的特点:

  • 静态成员函数内部不能使用非静态的成员变量和非静态的成员函数
  • 静态成员函数内部只能调用静态数据成员和静态的成员函数

非静态成员函数可以调用静态成员函数

如果一定要在静态成员函数中调用非静态成员函数,可以在静态成员函数中传参(对象),从而调用参数的函数

static 类型 函数名(参数列表) 
{    
    函数体 
}

原因在于静态成员函数的参数列表中不含有隐含的 this 指针,如下:

class Computer { 
public:    
    Computer(const char * brand, double price)    
    : _brand(new char[strlen(brand) + 1]())    
    , _price(price)    
    {        
        _totalPrice += _price;    
    }    
    //...    
    
    static void printTotalPrice()    
    {        
        cout << "总价:" << _totalPrice << endl;    
    }    
    //... 
    
private:    
    char * _brand;    
    double _price;    
    static double _totalPrice; 
};

对于静态成员函数,还可以直接通过类名进行调用,这也是最常用的方式:

//静态成员函数不用创建对象而能调用类中的函数
int main(void) 
{    
    Computer pc1("Huawei MateBook14", 5699);    
    pc1.print();    
    Computer::printTotalPrice(); //通过类名直接调用    
    return 0; 
}

2.const成员函数

const 在类成员函数中还有种特殊的用法,把const关键字放在函数的参数表和函数体之间(与之前介绍的 const放在函数前修饰返回值不同), 称为const成员函数,其具有以下特点:

  • 只能读取类数据成员,而不能修改之。
  • 只能调用const成员函数,不能调用非 const成员函数。
类型 函数名(参数列表) const 
{    
    函数体 
}
class Computer 
{ 
public:    
    //...   
    //const版本的成员函数可以与非const的版本的成员函数进行重载
    //一般建议先写const版本的成员函数,有需要再写非const版本
    //特别地,如果不修改数据成员,就定义const版本的函数
    void print()
    {
    }
    void print() const    
    {        
        cout << "品牌:" << _brand << endl             
             << "价格:" << _price << endl;    
    }    
    //... 
};

//默认情况下,非const对象可以调用非const版本的成员函数
//非const对象可以调用const版本的成员函数
Computer com1("Thinkpad", 8000);
com1.print();

//const对象可以调用const版本的成员函数
//const对象不能调用非cosnt版本的成员函数
const Computer com2("xiaomi", 6000);
com.print();

八、对象的组织

有了自己定义的类,或者使用别人定义好的类创建对象,其机制与使用int等创建普通变量几乎完全一 致,同样可以创建const对象、创建指向对象的指针、创建对象数组,还可使用 newdelete等创建动态对象。

1.const对象

const Point pt(1, 2); 
pt.print();

2.指向对象的指针

类名 *  指针名 [=初始化表达式];

Point pt(1, 2);                 
Point * p1 = nullptr; 
Point * p2 = &pt; 
Point * p3 = new Point(3, 4); 
Point * p4 = new Point[5];

p3->print();//合法 
(*p3).print();//合法

3.对象数组

类名 数组名[对象个数];

Point pts[2] = {Point(1, 2), Point(3, 4)}; 
Point pts[] = {Point(1, 2), Point(3, 4)}; 
Point pts[5] = {Point(1, 2), Point(3, 4)}; 
//或者 
//Point pts[2] = {(1,2),(3,4)};   //(❌)error 注意小括号内的逗号运算符
Point pts[2] = {{1, 2}, {3, 4}};  //注意是大括号
Point pts[] = {{1, 2}, {3, 4}}; 
Point pts[5] = {{1, 2}, {3, 4}}; 

4.堆对象

和把一个简单变量创建在动态存储区一样,可以用newdelete表达式为对象分配动态存储区,

void test() 
{    
    Point * pt1 = new Point(11, 12);//单个堆对象  调用构造函数    
    pt1->print();    
    delete pt1;    
    pt1 = nullptr;        
    
    Point * pt2 = new Point[5]();//堆对象数组  注意,不能进行赋值   
    pt2->print();    
    (pt2 + 1)->print();        
    delete [] pt2; //new和delete配合,不能用free释放
}

注意: 使用new表达式为对象数组分配动态空间时,不能显式调用对象的构造函数,因此,对象要么没 有定义任何形式的构造函数(由编译器缺省提供),要么显式定义了一个(且只能有一个)所有参数都 有缺省值的构造函数。

九、单例模式

单例模式是23种GOF模式中最简单的设计模式之一,属于创建型模式,它提供了一种创建对象的方式, 确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象

现步骤有如下三步:

  1. 将构造函数私有化
  2. 在类中定义一个静态的指向本类型的指针变量
  3. 定义一个返回值为类指针的静态成员函数
//单例模式的要求:一个类只能创建一个对象
//不能是栈对象,不能是全局对象,只能是堆对象  栈对象、全局对象系统自动分配空间
//
//用途:全局唯一的资源,全局唯一变量
//字典库、词典库、网页库、日志系统记录日志的对象

class Singleton
{
public:
    //静态成员函数可以在类外不创建类的对象而调用类中的函数
    static Singleton *getInstance()
    {
        if(nullptr == _pInstance)
        {
            _pInstance = new Singleton();//类的内部访问构造函数
        }
        return _pInstance;
    }
    static void destroy()
    {
        if(_pInstance)
        {
        	delete _pInstance; //调用private中的析构函数
      		_pInstance = nullptr;
        } 
    }
private:
    //构造函数私有,不能在类的外面访问
    Singleton()
    {
        cout << "Singleton()" << endl;
    }
    ~Singleton()  //析构函数放在private里面,只能类内调用			  
    {
        cout << "~singleton()" << endl;
    }
private:
    //仅是声明,没有定义
    //static int *s1;
    static Singleton *_pInstance; //静态成员函数需要静态的成员
};

//类外的定义才是给静态成员变量分配空间
// int *s1 = nullptr;
Singleton * Singleton::_pInstance = nullptr;//静态成员进行初始化

//Singleton gS1;//全局对象,error,不能让其编译通过,只能让构造函数私有
//Singleton gS2;//全局对象

int main()
{
    //Singleton s1;//栈对象,error,不能让其编译通过,只能让构造函数私有
    //Singleton s2;//栈对象
    //Singleton *ps1 = new Singleton(); //堆对象。类的外面创建不行,构造函数为私有
    
    Singleton *ps1 = Singleton::getInstance();
    Singleton *ps2 = Singleton::getInstance();
    cout << "ps1 = " << ps1 << endl
         << "ps2 = " << ps2 << endl;
    
    //delete ps1;  //new两次指向同一空间,delete两次的时候会出现两次析构函数
    //delete ps2;  //把析构函数放入private中,就只能类内调用了
    Singleton::destory();
    return 0;
}

//静态成员函数需要静态的成员
};

//类外的定义才是给静态成员变量分配空间
// int *s1 = nullptr;
Singleton * Singleton::_pInstance = nullptr;//静态成员进行初始化

//Singleton gS1;//全局对象,error,不能让其编译通过,只能让构造函数私有
//Singleton gS2;//全局对象

int main()
{
//Singleton s1;//栈对象,error,不能让其编译通过,只能让构造函数私有
//Singleton s2;//栈对象
//Singleton *ps1 = new Singleton(); //堆对象。类的外面创建不行,构造函数为私有

Singleton *ps1 = Singleton::getInstance();
Singleton *ps2 = Singleton::getInstance();
cout << "ps1 = " << ps1 << endl
     << "ps2 = " << ps2 << endl;

//delete ps1;  //new两次指向同一空间,delete两次的时候会出现两次析构函数
//delete ps2;  //把析构函数放入private中,就只能类内调用了
Singleton::destory();
return 0;

}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值