C++学习笔记-类与对象

来源:互联网 发布:诺基亚7230软件下载 编辑:程序博客网 时间:2024/06/11 19:17

类概述

面向对象与面向过程

  1. 结构化程序设计:考虑问题以算法为中心,而不是以数据为中心:

    程序 = 算法 + 数据结构。

    数据与数据处理分离导致面向过程程序设计存在缺点

  2. 面向对象程序设计:对象由相关的数据和操作组成,对象具有相对独立性,对外提供一定的服务:

    程序 = 对象 + 对象 + … + 对象 + 发送消息
    对象 = 算法 + 数据

  3. 面向对象的四大特性:抽象、封装、继承、多态

类基本概念

类其实与数学公式、公理是一样的,在数学公式,是数学家或者物理学家根据已有的现象,通过推理统计总结出来的一个共有性的东西,然后我们又可以利用这些公式去知道物理实验,这是一个抽象、应用的过程。同样的,C++用类来描述对象,类是对现实世界中相似事物的抽象。

类的定义分为两个部分:数据(相当于属性)和对数据的操作(相当于行为)。

一个类的人生路:

  • 定义类:
    C++中,分别用数据成员和函数成员来表现对象的属性和行为。
  • 实现类:
    进一步定义类的成员函数,使各个成员函数实现接口对外提供的功能
  • 使用类:
    通过该类声明一个属于该类的变量(即对象),并调用其接口(即public型的数据成员或函数成员)。

类定义语法

class 类名{private:       私有成员变量和函数protected:       保护成员变量和函数public:       公共成员变量和函数};  // 这个分好千万不能漏掉,用IDE用多了,这个很容易忽略

类的创建与销毁—构造函数与析构函数

构造函数

当对象被创建时,构造函数自动被调用。它是特殊的成员函数,与类同名,没有返回值,一个类可以有多个构造函数(构成重载)。

构造函数的执行分为两个阶段:

  1. 初始化段(初始化列表中):这里面的是真正的初始化

  2. 普通计算段(构造函数体内):在构造函数体内的不是初始化,只是普通赋值。

默认构造函数:

不带参数的构造函数称为默认构造函数。一个类必须包含构造函数,所以如果我们一个构造函数也不提供,编译器会默认提供一个空的构造函数(默认构造函数),但是一旦编写者提供了一个构造函数,编译器就不会再提供默认构造函数。

初始化列表:

数据成员初始化表可用于初始化类的任意数据成员(static数据成员除外),该表达式由逗号分隔的数据成员表组成,初值放在一对圆括号中。它位于构造函数的头和体之间,并用冒号将其与构造函数的头分隔开。

class Test{public:    Test(const int max) : num_(0), max_(max) {}    ~Test() {}private:    int num_;    const max_;}

初始化的时候先构造数据成员再构造自身。析构的时候先析构自身再析构数据成员。

当对象的数据成员是类对象时,如果这个对象成员有默认构造函数,即使初始化列表中没有显式初始化这个对象成员,编译器也会自动调用这个对象成员的默认构造函数,如果这个对象成员没有默认构造函数,又没在初始化列表中显示初始化这个对象成员,就会编译出错。这与auto数据成员不同,auto数据成员当你不显式去初始化它的时候,编译器也不会管它:

class Object1{public:    // 枚举成员,枚举常量TYPE_A和TYPE_B的可见域是该类,它与static成员一样,    // 可以直接通过Object::TYPE_A访问,也可以obj1.TYPE_A来访问,因为没有    // 定义具体的枚举变量,所以Object类的大小是12bytes    enum    {        TYPE_A = 100,        TYPE_B = 200    };public:    // 程序员提供了构造函数,编译器不会再提供默认构造函数    explicit Object1(int num)         : num_(num), const_num_(num), ref_num_(num)    {        cout << "Object ... " <<  num_ << endl;    }    ~Object1()    {        cout << "~Object ..." <<  num_ << endl;    }private:    int num_;    const int const_num_; // 只作用于对象内部,类的不同对象之间可以不同。    int& ref_num_;};class Object2 {    // 这个构造函数虽然有参数,但是提供了默认值,可以作为默认构造函数使用    // 此时如果还要定义默认构造函数(无参版本),就要小心了,调用不慎会产生    // 歧义。这里只是为了演示,具体设计时这种情况必须避免。    Object2(int num = 0) {}};class Container{public:    // 因为obj1对应的类没有默认构造函数,所以这个对象成员必须在初始化列表中    // 显示初始化,并提供必要的参数;而obj2由于有默认构造函数,即使没有在初    // 始化列表中显示初始化,编译器也会自动调用它的默认构造函数来初始化它,    // 效果等同于在初始化列表中添加 , obj2()。    //    // 先构造obj1,在再构造obj2,这取决于声明顺序,最后构造本身。析构的时候    // 顺序反转,先析构自身,然后析构obj2,最后析构obj1。    Container(int num) : obj1_(num) /*, obj2()*/    {        cout << "Container" << endl;    }    ~Container()    {        cout << "~Container" << endl;    }private:    Object1 obj1_;    Object2 obj2_;};int main(void){    Container container(10);    Object1 obj1(10);    Object2 obj2;    cout << obj1.TYPE_A << " | " << obj1.TYPE_B << endl;    cout << Object1::TYPE_A << endl; // 常量可以这样用    return 0;}

注意:

  • 每个成员在初始化表中只能出现一次。

  • 如果提供了带参数构造函数,并且形参都有默认值,那就相当于提供了默认构造函数,因为可以什么参数也不用输入;如果此时又提供了一个默认构造函数(无参数),当构造对象的时候,如果不传递值,就会出错,因为发生了歧义。

构造函数一般声明为公有,否则不能被外部调用。如果不希望类在外部被实例化,可以将构造函数声明为私有,同样的如果不希望类对象被拷贝,就把拷贝构造函数和重载的赋值运算符声明为私有。

析构函数

析构函数与类同名,之前冠以波浪号,以区别于构造函数。

析构函数没有返回类型,也不能指定参数,因此,析构函数只能有一个,不能被重载。

对象超出其作用域被销毁时,析构函数会被自动调用

缺省析构函数:

如果用户没有显式地定义析构函数,编译器将为类生成“缺省析构函数”,缺省析构函数是个空的函数体,只清除类的数据成员所占据的空间,但对类的成员变量通过new和malloc动态申请的内存无能为力,因此,对于动态申请的内存,应在类的析构函数中通过delete或free进行释放,这样能有效避免对象撤销造成的内存泄漏。

析构函数与数组:
数组创建时会顺次调用每一个元素的构造函数,delete时一样也是顺次调用析构函数。

显示调用析构函数
程序员不能显式调用构造函数,但却可以调用析构函数控制对象的撤销,释放对象所占据的内存空间,以更高效地利用内存,如:

#include "computer.h"int main(){    computer comp("Dell",7000);    comp.print();    comp.~computer(); // 显式调用析构函数,comp被撤销    return 0;}   // 这里comp对象释放时析构函数也被隐式调用了一次

虽然可以显式调用析构函数,但不推荐这样做,因为可能带来重复释放指针类型变量所指向的内存等问题。

析构函数何时被调用:

  • 对于全局定义的对象,每当程序开始运行,在主函数main执行之前,就调用全局对象的构造函数。整个程序结束时调用析构函数。

  • 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部变量的作用域时调用对象的析构函数。

  • 对于关键字static修饰的静态局部变量,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。

  • 对于用new运算符创建的对象,每当创建该对象时调用构造函数,当用delete删除该对象时,调用析构函数。

总之一句话,对象生命周期结束时,调用析构函数。


类的拷贝-拷贝构造函数与赋值运算符

拷贝构造函数

拷贝构造函数是特殊的构造函数,所以它遵循构造函数的一切规则,只是参数是一个同类型的其它对象而已:

#include <cstring>#include <iostream> class Book{    typedef unsigned int uint;    friend std::ostream& operator<< (std::ostream& cout, const Book& book);public:    Book(const char* id, uint price)        : id_(NULL), price_(price)    {        std::size_t id_length = strlen(id) + 1;        id_ = new char[id_length];        memcpy(id_, id, id_length);    }    Book(const Book& other_book)         : id_(other_book.id_), price_(other_book.price_)    {}    ~Book()    {        if (id_) {            delete[] id_;        }    }    void PrintIdAddr()    {        std::cout.unsetf(std::ios::dec);        std::cout.setf(std::ios::hex);        std::cout << "0x" << reinterpret_cast<unsigned long>(id_)                   << std::endl;        std::cout.unsetf(std::ios::hex);        std::cout.setf(std::ios::dec);    }private:    char* id_;    uint price_;    };std::ostream& operator<< (std::ostream& cout, const Book& book){    return cout << "(" << book.id_ << " , " << book.price_ << ")" << std::endl;}int main(void){    Book book1("book", 100);    Book book2(book1);    std::cout << book1;    book1.PrintIdAddr();    std::cout << book2;    book2.PrintIdAddr();    return 0;}// OUT:// (book, 100)// 0x7f2550// (book, 100)// 0x7f2550

其中的Book(const Book& other_book);就是一个拷贝构造函数或者成为复制构造函数。

但是上面的拷贝构造函数有个问题,程序能正常运行的原因是我们析构函数中释放堆内存的时候做了判断,但它仍然有潜在的风险:

这个类有一个指针成员,而我们的拷贝构造函数在初始化这个指针的时候是直接进行的值复制(与编译器默认提供的一样),这样将导致不相关的两个对象book1和book2共享了一块堆内存,也就是字符串”book”所在的内存,这样当book1的析构函数被调用后,会导致book2中的id_成为野指针,再显示id_的时候,就会发现内容是乱掉的。而且如果析构函数中不做判断,还会引起程序崩溃。

上面的程序因为book1和book2位于同一作用域,所以不会发生上述现象,我们稍微动一下代码,就会发现问题:

int main(void){    Book book1("book", 100);    Book book2(book1);    std::cout << book1;    book1.PrintIdAddr();    book1.~Book();    std::cout << book2;    book2.PrintIdAddr();    return 0;}// OUT:// (book, 100)// 0x7f2550// (乱七八糟的一堆无意义字符, 100)// 0x7f2550

我们显式调用析构函数,就会发现问题了。可能有的人会说,谁没事去显式调用析构函数,其实当使用placement new的时候就需要显示调用析构函数(运算符重载部分会说),当然编码本就应该秉着严谨的态度,潜在的问题说不定将来某个时候就会变为大问题,这里的拷贝构造函数应该改成默认构造函数(第一个)那样。

而如果我们不提供拷贝构造函数,编译器默认提供的拷贝构造函数,对于指针成员就是这样直接进行值拷贝的,用专业术语来说就是浅拷贝,所以对于指针成员,我们还是自己提供拷贝构造函数比较好,能不依赖编译器就不要依赖编译器。

注意:
1. 拷贝构造函数的参数必须是引用,不能传值,因为传值的时候实参初始化形参又会调用拷贝构造函数,那样就会出现死循环;
2. 如果不希望Book book2 = book1;(调用拷贝构造函数,不是赋值)这种用法,可以在拷贝构造函数前面加explicit进行修饰。

以下情况下拷贝构造函数会被调用:

  • 函数形参是类对象,实参初始化形参时会调用拷贝构造函数;
  • 函数返回类对象时,会先调用拷贝构造函数,构造一个新的临时对象,然后返回没这个临时对象(如果返回值没被引用,会立即调用析构函数销毁,如果有const引用来引用这个临时变量,它就不会被立即销毁,注意,只能是const引用才能引用临时变量);
  • 定义类对象时,使用其它对象初始化该对象时。

转换构造函数

单个参数的构造函数称为转换构造函数,因为它可以将其它数据类型的对象转换为该类的对象,既然是构造函数,当然是在初始化的时候转换的才会调用转换构造函数,如果想要赋值时进行转换,那就要通过重载赋值运算符来实现了。

class test{public:    explicit Test(int num) : num_(num) {} // 转换构造函数private:    int num_;}

同样的,不加explicit(只用于类的构造函数)的话,可以用Test obj = 10Test obj(10)两种形式来初始化,如果加了explicit就只能用第二种了,explicit的目的就是防止隐式转换

类的赋值运算符

运算符重载与函数没啥不同,只是如果运算符左操作数是类对象本身,最好是重载为成员函数,如赋值运算符、“[]”运算符等;如果左操作数不一定是类对象本身,最好重载为友元函数的形式,比如流运算符、加减乘除运算符等。

在上上个小节的例子基础上加:

#include <cstring>#include <iostream> class Book{    typedef unsigned int uint;    friend std::ostream& operator<< (std::ostream& cout, const Book& book);public:    Book(const char* id, uint price)        : id_(NULL), price_(price)    {        std::size_t id_length = strlen(id) + 1;        id_ = new char[id_length];        memcpy(id_, id, id_length);    }    explicit Book(const Book& other_book)         : id_(NULL), price_(other_book.price_)    {        std::size_t id_length = strlen(other_book.id_) + 1;        id_ = new char[id_length];        memcpy(id_, other_book.id_, id_length);    }    void PrintIdAddr()    {        std::cout.unsetf(std::ios::dec);        std::cout.setf(std::ios::hex);        std::cout << "0x" << reinterpret_cast<unsigned long>(id_) << std::endl;        std::cout.unsetf(std::ios::hex);        std::cout.setf(std::ios::dec);    }    ~Book()    {        if (id_) {            delete[] id_;        }    }    // 赋值运算符,左操作数为类对象本身,重载为成员函数的形式    Book& operator= (const Book& other_book)    {        if (id_) {            delete[] id_;        }        std::size_t id_length = strlen(other_book.id_) + 1;        this->id_ = new char[id_length];        memcpy(this->id_, other_book.id_, id_length);        this->price_ = other_book.price_;        return *this;    }private:    char* id_;    uint price_;    };std::ostream& operator<< (std::ostream& cout, const Book& book){    return cout << "(" << book.id_ << " , " << book.price_ << ")" << std::endl;}int main(void){    Book book1("book", 100);    std::cout << book1;    book1.PrintIdAddr();    Book book2 = book1;         // 调用拷贝构造函数    book2 = Book("book2", 200); // 调用重载的赋值运算符    std::cout << book2;    book2.PrintIdAddr();    return 0;}// OUT:// (book, 100)// 0x2f2550// (book2, 200)  // 不是(book, 100)了// 0x2f2630 // 两个指针指向不同的堆内存

还有一点要说的是,由于成员函数的const版本和非const版本构成承载,而运算符也可以看成函数,所以它们的const与非const版本也可以同时存在,当两个同时存在时,如果操作数是const的则只能调用const版本,如果没提供const版本,会报错(但取地址符&不会,编译器会默认提供const与非const两个版本);非const操作数,会优先调用非const版本,如果没有,则调用const版本,但是如果两个版本的返回值不同,则要注意了。

const对象除了构造函数和析构函数之外,只能调用const成员函数。


空类默认产生的内容

class Empty {};
  1. 默认构造函数:Empty();
  2. 默认拷贝构造函数:Empty(const Empty&);
  3. 默认析构函数:~Empty();
  4. 默认赋值运算符:Empty& operator= (const Empty&);
  5. 取址运算符:Empty* operator& ();
  6. 取址运算符const:const Empty* operator& () const;

默认产生的这些函数,都是公有的。

类中的特殊成员

  1. static数据成员
  2. static成员函数
  3. const成员
  4. const成员函数
  5. 引用成员
  6. 类对象成员

static数据成员:

  • 与类关联,不于类对象关联,被类的所有对象共享;

  • 类的static成员在类体中声明,在类体外的文件作用域中定义与初始化(全局的),必须是在文件域,不能再函数中定义和初始化。

    特殊情况:
    static const 整型成员(char、short、long等都是整型)可以在类体内幕定义并初始化,不需要在类体外定义及初始化。

  • 处于类作用域中,可以避免与其它类的成员或者全局变量出现名称冲突哦,此时的类对于static成员来说就相当于一个名称空间,该static成员的可见域就是这个类,但其生存期是整个程序运行期;

  • static数据成员可以私有,封装性好,配合static函数,可以方便获取其值;

class StaticTest{private:    static int xpos_;};int StaticTest::xpos_ = 0; // 不需要再加static

static成员函数:

  • static成员函数中没有this指针,所以static成员函数不能访问非static数据成员,因为非static数据成员是对象域的,只有通过this指针才能访问;但非static成员函数
    可以调用static数据成员;

  • 可以通过类::static成员函数名()来访问,也可以通过对象.static成员函数名()或者对象指针->static成员函数名()来调用。

const数据成员:

前面在构造函数的初始化列表中已经提到,类的const数据成员,只能在构造函数的初始化列表中进行初始化,即使在构造函数体内也不能进行赋值操作。

const成员函数:

  • 声明为const的成员函数,不会改变类的状态(类的状态用其数据成员的值来表征,也就是说它只能读取数据成员的值,不能改变它们);

  • 声明为const的成员函数,只能调用const成员函数,不能调用非const成员函数;

  • 类外定义时,不能省掉const关键字,因为带const与不带const构成重载。

class ConstTest{public:    void Print() const;private:    int num_;}void ConstTest::Print() const // const不能省略{    std::cout << num_ << std::endl;}

引用数据成员

引用成员只能在构造函数初始化列表中初始化。

类对象数据成员

类在构造的时候,先调用类对象成员的构造函数(按照类中声明的顺序)构造类对象成员,再构造自身;析构的时候先调用自身的析构函数销毁自己,然后调用类对象成员的析构函数销毁这些类对象成员,顺序与构造是相反。


类大小的计算

  • 遵循结构体对齐原则

  • 只与数据成员有关,与成员函数无关,与static数据成员无关,因为static数据成员存于.data/.bss区,而其它数据成员存于堆区或者栈区,类的大小(sizeof)是指堆或栈上所占的内存空间;

  • C++将类中的引用成员当成指针来处理,所以引用成员会占据4字节(32位系统)或者8字节(64位系统)

class Test{public:    Test(const int num) : num_(num) {}private:    int num_;};int main(void){    std::cout << sizoef(Test) << std::endl;    return 0;}// OUT:// 8     // 系统是linux x86_64
  • 虚函数对类的大小有影响,每一个虚函数会占用一个指针变量大小的内存空间(4bytes/8bytes),用于指向虚函数表;

  • 虚继承对类的大小有影响,占用一个指针变量的大小,指向虚表。


单例设计模式

单例模式是一种设计模式,它保证类只有一个实例,并提供一个全局访问点。

  • 将默认构造函数、拷贝构造函数、赋值运算符声明为私有,即可保证单例;
  • 定义一个静态成员函数和一个静态数据成员,用于获取实例。
#include <iostream>class Singleton{public:    static Singleton& GetInstance()    {        if (instance_ == NULL) {            instance_ = new Singleton;        }        return *instance_;    }    ~Singleton()    {        if (instance_ != NULL) {            delete instance_;        }    }private:    Singleton() {        std::cout << "Singleton" << std::endl;    };    Singleton(const Singleton& other) {};    Singleton& operator= (const Singleton& other) {};private:    static Singleton* instance_;};Singleton* Singleton::instance_ = NULL;int main(void){    Singleton &s1 = Singleton::GetInstance();    Singleton &s2 = Singleton::GetInstance();    return 0;}// OUT:// Singleton// 005F2550 005F2550

可见s1和s2指向同一内存地址,而且构造函数只调用了一次,这就是单例模式。但是有个问题,就是析构函数不会被调用,因为Singleton本身没定义对象,所以谈不上析构函数,而静态数据成员是个指针,自然不会自动析构。为了解决这个问题,我们将代码修改为:

#include <iostream>class Singleton{public:    class Garbo {    public:        ~Garbo()        {            if (instance_ != NULL) {                delete instance_;            }        }    };public:    static Singleton& GetInstance()    {        if (instance_ == NULL) {            instance_ = new Singleton;        }        return *instance_;    }    ~Singleton()    {        if (instance_ != NULL) {            std::cout << "~Singleton" << std::endl;        }    }private:    Singleton() {        std::cout << "Singleton" << std::endl;    }    Singleton(const Singleton& other) {}    Singleton& operator= (const Singleton& other) {}private:    static Singleton* instance_;    static Garbo garbo_;};Singleton* Singleton::instance_ = NULL;Singleton::Garbo Singleton::garbo_;int main(void){    Singleton &s1 = Singleton::GetInstance();    Singleton &s2 = Singleton::GetInstance();    std::cout << &s1 << " " << &s2 << std::endl;    return 0;}// OUT:// Singleton// ~Singleton

注意:此时~Singleton()里不要在delete了。虽然堆空间不能自动释放,但是栈空间可以自动释放,这里就是利用了garbo_成员销毁时会自动调用析构函数来实现的。

当然也可以用C++11的智能指针来实现:

#include <iostream>#include <memory>class Singleton{public:    static Singleton& GetInstance()    {        if (instance_ == nullptr) {            instance_ = std::unique_ptr<Singleton>(new Singleton);        }        return *instance_.get();    }    ~Singleton()    {        std::cout << "~Singleton" << std::endl;    }private:    Singleton()    {        std::cout << "Singleton" << std::endl;    }    Singleton(const Singleton& other);    Singleton&operator=(const Singleton& other);    static std::unique_ptr<Singleton> instance_;};std::unique_ptr<Singleton> Singleton::instance_;int main(void){    Singleton &s1 = Singleton::GetInstance();    Singleton &s2 = Singleton::GetInstance();    return 0;}// OUT:// Singleton// ~Singleton

效果与前面的代码一样。


static总结

  1. C中的用法:
    • 函数内部修饰局部变量,使得局部变量的生命周期长于函数(与程序生命周期一样),类似于记忆元件,导致函数不可重入,也不是线程安全;
    • 修饰全局变量或者函数,表示它们只在本文件可见,其它文件见不到也无法访问它们(internal linkage)
  2. C++中新增的用法:
    • 修饰类的数据成员,生存期为整个程序的生命周期,每个类只有一份,所有类对象共享这一份静态数据;
    • 修饰类的成员函数,即静态成员函数,静态成员函数只能访问类的静态成员和静态成员函数,不能访问非静态成员函数和非静态数据成员,因为没有this指针 。

类的作用域

类定义的作用域与可见域

和函数一样,类定义没有生存期的概念,但有作用域与可见域;类可被定义在三种作用域中,这也是类定义的作用域:

1. 全局作用域:
在函数和其他类定义的外部定义的类称为全局类,绝大多数的C++类是定义在该作用域中,我们在前面定义的所有类都是在全局作用域中,全局类具有全局作用域。

2. 类作用域:
一个类可以定义在另一个类的定义中,即所谓的嵌套类,举例来说,如果类A定义在类B中,并且B的访问权限是public,则A的作用域可认为和B的作用域相同,不同之处在于必须使用B::A的形式访问A的类名,这时的B类似于命名空间。当然,如果B的访问权限是private,则只能在A类内部使用类名创建该类的对象,无法在外部创建B类的对象。嵌套类的成员函数可以在类体外定义:

class Outer // 外围类{    class Inner // 嵌套类,此处为私有    {    public:        void Func();    };public:    Inner obj_;    void Func()    {        std::cout << "Outer" << std::endl;        obj_.Func(); // 外围类调用嵌套类的成员函数    }};// 在外部定义嵌套类的成员函数时,需要加对应的外围类的名字作为限定void Outer::Inner::Func() {    std::cout << "inner" << std::endl;}

3. 块作用域:
类的定义可以在代码块(如函数)中,这是所谓局部类,该类完全被块包含,其作用域仅仅限于定义所在块,不能在块外使用类名声明该类的对象。局部类的成员函数必须定义在类体中,局部类中不可以有静态成员。

int Func(){    class LocalClass // 只能在Func函数内使用    {    public:        LocalClass (int num) : num_(num)        {        }        void Display() const        {            std::cout << "num_: " << num_ << std::endl;        }    private:        int num_;        // static int i_; // Error:局部类不允许有静态成员    };    LocalClass lc(10);    lc.Display();}

注:类名也存在覆盖
和普通变量的覆盖原则一样,类名也存在“屏蔽”和“覆盖”,不过,依旧可使用作用域声明符“::”指定具体使用的类名,如“::类名”访问的是全局类,使用“外部类::嵌套类”访问嵌套类。

类对象的生存期、作用域、可见域

类名无生存期,只有作用域和可见域。

对象有生存期,对象的生存期也是对象中所有非静态成员变量的生存期,这些成员变量都随着对象的创建而创建,随着对象的撤销而撤销。

对象的生存期、作用域和可见域取决于对象的创建位置,同样有全局、局部、类内之分,和普通变量并无区别。

类对象的声明一定要位于类定义之后:

class B; //前向声明,避免循环依赖B objectB; //创建B类的对象。错误class B{    // ……};

上述代码是不对的,因为仅仅靠声明,编译器无法知道究竟需要为对象分配多大的空间。但如果只是定义对象的引用或者指针,则只需要位于类声明之后即可:

class B;       // 前向声明B *pB = NULL;  // 创建B类对象的指针。  正确B *pC = new B; // 创建B类对象。       错误class B{    // ……};

友元

友元允许非类成员函数访问类的公有成员的一种机制,友元的形式可以是函数或者类。

定义友元函数:

在类的定义中用friend声明了一个外部函数或其他类的成员函数(public和private均可)后,这个外部函数称为类的友元函数。友元函数声明的基本格式为:friend 函数原型;友元函数中可访问类的private成员。

A类的成员函数作为B类的友元函数时,必须先定义A类,而不仅仅是声明它。

注意:将其他类的成员函数申明为本类的友元函数后,该友元函数并不能变成本类的成员函数。

#include <iostream>#include <cmath>using std::cout;using std::endl;class Point; // 前向声明Point类class Line{public:    float dis(Point& p1, Point& p2); // 友元函数的原型,作为Line类的成员函数};class Point{    // 友元的声明    friend float Line::Distance(const Point &p1, const Point &p2);     friend float Distance(const Point &p1, const Point &p2)    {        // 可以访问Point类对象的private成员x_, y_        float d = sqrt((p1.x_ - p2.x_) * (p1.x_ - p2.x_)                 + (p1.y_ - p2.y_) * (p1.y_ - p2.y_));        return d;    }public:    Point(float x = 0.0f, float y = 0.0f) : x_(x), y_(y)    {}    void Disp()    {        cout << "(" << x_ << "," << y_ << ")";    }private:    float x_, y_;};// Line类成员函数dis的实现,作为Point类的友元函数float Line::Distance(const Point &p1, const Point &p2){    // 可以访问Point类对象的private成员x_, y_    float d = sqrt((p1.x_ - p2.x_) * (p1.x_ - p2.x_)             + (p1.y_ - p2.y_) * (p1.y_ - p2.y_));    return d;}int main(void){    Point p1(0,0), p2(3,4);    Line line;    cout << line.Distance(p1, p2) << endl;    cout << Distance(p1, p2) << endl;    return 0;}

上述代码中,Line类的成员函数dis(…)的实现必须在类外进行,且必须在Point类的定义之后,因为其参数中包含了Point这种类型;但普通的友元函数dis(…),可以直接在类内定义,当然也可以在类外定义

Line类的dis()函数本来是不能访问Point.x和Pint.y这种Point类的private成员的,但在Point类中将dis()申明为友元函数后就能访问了。但dis()函数依然不是Point类的成员函数。

友元函数使得类不需要提供额外的私有成员访问接口,可以提高效率,但它也破坏了封装性,不是必须,最好不要用。

友元函数的重载:

要想使得一组重载函数全部成为类的友元,必须一 一声明,否则只有匹配的那个函数会成为类的友元,其它的仍然是普通函数:

class Exp{    firend void test(int);  };void test();void test(int);void test(double);

上述代码中,只有void test(int)函数是Exp类的友元函数,void test()`void test(double)函数只是普通函数。

友元类

类A作为类B的友元时,类A称为友元类。A中的所有成员函数都是B的友元函数,都可以访问B中的所有成员。同样的在public部分声明与在private部分声明没有区别。声明方式:

class Line;class Point {    friend class Line; // 友元类的声明,位置同样不受限制public:    Point(int x = 0, int y = 0) : x_(x), y_(y)    {}    void Disp()    {        cout << "(" << x_ << "," << y_ << ")";    }private:    int x_, y_;};class Line  //类CLine的定义,其中所有的函数都是CPoint类的友元函数{public:    // 可访问p1和p2的private成员    float Distance(const Point& p1, const Point& p2)     {        float d;        d = sqrt((p1.x_-p2.x_) * (p1.x_-p2.x_)                +(p1.y_-p2.y_) * (p1.y_-p2.y_));        return d;    }    void Set(Point* p1, int a, int b) // 可访问p1和p2的private成员    {        p1->x_ = a;        p1->y_ = b;    }};

友元关系的特点:

  • 友元关系是单向的:
    A是B的友元类,并不代表B是A的友元类。

  • 友元关系不能被传递:
    A是B的友元类,B又是C的友元类,并不代表A是C的友元类。

  • 友元关系不能被继承:
    A是B的友元类,C继承自A,并不代表C是B的友元类;
    A是B的友元类,C继承自B,并不代表A是C的友元类。


运算符重载

前面提到过,运算符其实就是函数,所以运算符重载只是为了语法上的方便,使得用户自定义的类型使用起来更像内置类型。既然就是个函数调用,那按照C++规定的语法来写就行了,其它的与函数没什么区别。

运算符重载规则:

  • 可以重载的运算符:
    双目运算符 + - * / %
    关系运算符 == != < > <= >=
    逻辑运算符 || && +
    单目运算符 + - * &
    自增自减运算符 ++ --
    位运算符 | & ~ ^ << >>
    赋值运算符 = += -= *= /= %= &= |= ^= <<= >>=
    空间申请和释放 new delete new[] delete[]
    其他运算符 () -> ->* , []

  • 不可以重载的运算符:
    域运算符 ::
    条件运算符 ?:
    成员访问运算符 .
    成员指针访问运算符 .*
    sizeof关键字

  • 重载运算符必须至少包含一个类类型或者枚举类型的参数;

int operator+ (const int &lhs, const int &rhs){    return lhs+ rhs;}// 编译错误: "error: overload 'operator+' must have at least one // parameter of class or enumeration type"
  • 不能创建新的运算符(只能重载内置的运算符);

  • 不允许改变运算符操作数的个数;

  • 重载后不改变优先级和结合性(但算数的求值顺序可以变,逻辑运算符也不再具备“短路”特性);

  • 单目运算符最好重载为成员函数,双目运算符最好重载为友元函数(zuo为对象本身,则适合成员函数形式,否则友元,不过需看具体需求,如+运算符适合友元形式);

  • 只能以类成员形式重载的运算符:
    赋值运算符 =
    下表运算符 []
    指针运算符 ->
    函数访问运算符 ()

  • 只能以友元函数形式重载的运算符:
    流运算符 << >>

对于其它的绝大多数可重载操作符来说,两种重载形式都是允许的。

各种运算符的重载

运算符重载的方式分为下面3种:
1. 采用普通函数的重载形式
2. 采用友元函数的重载形式
3. 采用成员函数的重载形式

用成员函数重载双目运算符时,左操作数无须用参数输入,而是通过隐含的this指针传入,这种做法的效率比较高。其形式与普通成员函数没什么两样。

操作符还可重载为友元函数形式,这将没有隐含的参数this指针。对双目运算符,友元函数有2个参数,对单目运算符,友元函数有一个参数。形式与前面说的普通的友元函数没啥区别。

我们通过一个String类来看看赋值运算符=、下标运算符[]、+/+=运算符以及流运算符的使用:

string.h

#ifndef CPPBASIC_STRING_H_#define CPPBASIC_STRING_H_#include <iostream>using std::istream;using std::ostream;class String {public:    String();    String(const char *str);    String(const String &other);    ~String();    void Display() const;    String& operator= (const String &other);    bool operator! () const;    char& operator[] (unsigned int index);    const char& operator[] (unsigned int index) const;    String& operator+= (const String& other);    friend String operator+ (const String& str1, const String& str2);    friend ostream& operator<< (ostream& out, const String& string);    friend istream& operator>> (istream& in, String& string);private:    char *AllocAndCopy(const char *str);private:    char *str_;};#endif //CPPBASIC_STRING_H_

string.cpp

#include "String.h"#include <cstring>#include <iostream>using std::cout;using std::endl;String::String() {    cout << "construct default" << endl;    str_ = AllocAndCopy("");}String::String(const char *str) {    cout << "construct one para" << endl;    str_ = AllocAndCopy(str);}String::~String() {    cout << "delete" << endl;    delete [] str_;}String::String(const String &other){    cout << "construct copy" << endl;    str_ = AllocAndCopy(other.str_);}// 赋值运算符,只能重载为成员函数形式,返回引用是为了能实现a=b=c这种的连等。// 返回的引用不能使const,因为可能会被改变String& String::operator= (const String &other){    std::cout << "operator= ..." << std::endl;    if (this == &other) {        return *this;    }    delete [] str_;    str_ = AllocAndCopy(other.str_);    return *this;}// 非运算符,单目运算符,这里实际上改变了内置的语言,功能是字符串不为空返回真,// 为空返回假。bool String::operator! () const{    return strlen(str_) > 0;}// 下标运算符,只能重载为成员函数形式,返回字符串中对应位置处的字符,// 当然可以return str[index]来直接返回,这里是为了防止代码重复,// 让非const版本的运算符重载调用const版本的运算符重载,只不过由于// 本身代码少,看不出优势,一旦代码量增大,优势便会明显起来。char& String::operator[] (unsigned int index){    return const_cast<char&>(const_cast<const String&>(*this)[index]);}// const版本的下标运算符重载,只能重载为成员函数形式。const char& String::operator[] (unsigned int index) const{    return str_[index];}// '+'运算符与赋值运算符的合体,相当于在本字符串后面追加新的内容,它属于// 赋值运算符的范畴,所以最好重载为成员函数形式。String& String::operator+= (const String& other){    int len = strlen(str_) + strlen(other.str_) + 1;    char* newstr = new char[len];    memset(newstr, 0, len);    strcpy(newstr, str_);    strcat(newstr, other.str_);    delete [] str_;    str_ = newstr;    return *this;}// 加法运算符,前面说过,加法运算符最好重载为友元函数的形式,并且要返回值。String operator+ (const String& str1, const String& str2){    String tmp(str1);    tmp += str2;    return tmp;}// 流插入运算符,必须重载为友元函数的形式,返回引用是为了能// 用cout << ... <<< ...这种连续输出语句,输入参数中的流// 不能使const,返回的引用也不能是const,因为需要改变流状态ostream& operator<< (ostream& out, const String& string){    return (out << string.str_);}// 流提取运算符,只能重载为类成员函数,返回引用也是为了能使用// cin >> ... >>> ...这种形式的连用istream& operator>>(istream& in, String& string){    char tmp[2048] = {0};    in >> tmp;    string = tmp;    return in;}void String::Display() const {    cout << str_ << endl;}char *String::AllocAndCopy(const char *str){    int str_len = strlen(str) + 1;    char *tmp = new char[str_len];    memset(tmp, 0, str_len);    strcpy(tmp, str);    return tmp;}

指针运算符的重载,形式和含义比较特殊,可以实现外层类直接调用类对象成员的成员函数:

class DBHelper{public:    DBHelper()    {        cout << "DBHelper ..." << endl;    }    ~DBHelper()    {        cout << "~DBHelper ..." << endl;    }    void Open()    {        cout << "Open ..." << endl;    }    void Close()    {        cout << "Close ..." << endl;    }    void Query()    {        cout << "Query ..." << endl;    }};class DB // 相当于一个智能指针,包装了一个DBHelper指针。{public:    DB()    {        db_ = new DBHelper;    }    ~DB()    {        delete db_;    }    DBHelper* operator->() //  指针运算符重载    {        return db_;    }private:    DBHelper* db_;};int main(void){    DB db;    // 这些函数不是DB类的函数,而是他所包装的DBHelper类的函数    db->Open();  // 相当于 (db.operator->())->Open()    db->Query(); // 相当于 (db.operator->())->Query()    db->Close(); // 相当于 (db.operator->())->Close()}

下面我们通过一个整数类来看看自增运算符和类型转换运算符的用法:

Integer.h

#ifndef CPPBASIC_INTEGER_H_#define CPPBASIC_INTEGER_H_class Integer {public:    Integer(); // 默认构造函数    Integer(const Integer& other); // 拷贝构造函数    explicit Integer(int n = 0); // 转换构造函数    Integer& operator=(const Integer& other); // 赋值运算符    ~Integer();    void Display();    Integer& operator++();   // 前置++    Integer operator++(int); // 后置++    operator int();          // 类型转换运算符    friend Integer operator+(const Integer& obj1, const Integer& obj2);private:    int n_;};#endif //CPPBASIC_INTEGER_H_

Integer.cpp

#include "Integer.h"#include <iostream>using std::cout;using std::endl;Integer::Integer() : n_(0){    cout << "Integer() ..." << endl;}Integer::Integer(const Integer& other) : n_(other.n_){    cout << "Integer(Integer) ..." << endl;}Integer::Integer(int n) : n_(n){    cout << "Integer(int) ..." << endl;}Integer::~Integer(){    cout << "~Integer() ..." << endl;}Integer& Integer::operator=(const Integer& other){    cout << "Integer::operator= ..." << endl;    n_ = other.n_;    return *this;}void Integer::Display(){    std::cout << n_ << std::endl;}// 前置++,参数列表为空,实际上有一个隐含的this参数,前置++比较简单,// 直接int成员加一就行了,因为前置++没有延迟,会马上发生变化。Integer& Integer::operator++(){    ++n_;    return *this;}// 为了区分后置++与前置++,后置++的参数列表需要通过一个额外的int参数来区分,// 但这个int参数是没用的,只是用来区分。为了实现'带延迟的++',我们通过一个// 临时变量来实现,临时变量里面的int成员是原始值,然后当前类的int成员+1,// 返回的临时变量(用于计算)是旧值,而本身的值已经变为新值。Integer Integer::operator++ (int){    Integer tmp(n_);    n_++;    return tmp;}// 类型转换运算符,'必须是成员函数,没有参数,不能指定返回类型'。Integer::operator int(){    return n_;}Integer operator+(const Integer& obj1, const Integer& obj2){    Integer tmp(obj1.n_ + obj2.n_);    return tmp;}int main(void){    Integer n(100);    Integer n2;    cout << "=============" << endl;    n2 = n++; // <=> n.operator++(0);, 传的实参值无关结果    cout << "=============" << endl;    n.Display();    n2.Display();    return 0;}// OUT:// Integer(int) ...// Integer(int) ...// =================      //=========进入后置++函数体=====// Integer(int) ...       // 调用转换构造函数生成局部对象tmp // Integer(Integer) ...   // 调用拷贝构造函数生成临时对象//                        // 返回临时对象// ~Integer ...           // 销毁局部对象tmp//                        //=========退出后置++函数体=====// Integer::operator= ... // 将临时对象赋值给n2,调用赋值运算符// ~Integer ...           // 无const引用指向临时对象,所以立即销毁临时对象// ================= // 101                    // n++遇到分号,表示计算单元结束,已经+1// 100                    // n是返回值,是原值// ~Integer ...           // 销毁n2// ~Integer ...           // 销毁n

通过上述例子,我们也顺便回顾了下返回类对象值时发生的拷贝构造函数的调用过程。

new和delete运算符的重载:

通过重载new和delete,我们可以自己实现内存的管理策略。new和delete只能重载为类的静态运算符。而且重载时,无论是否显示指定static关键字,编译器都认为是静态的运算符重载函数。

重载new时,必须返回一个void *类型的指针,它可以带多个参数,但第1个参数必须是size_t类型,该参数的值由系统确定

static void* operator new (size_t nSize){    void* p = new char[nSize];    return p;}

重载delete时必须返回void类型,它可以带有多个参数,第1个参数必须是要释放的内存的地址void *,如果有第2个参数,它必须为size_t类型:

static void operator delete (void* p){    delete[] pVoid;}

上述的new操作符成为operator new,它在堆内存中新开辟一块内存空间,对应的要用operator delete来释放空间;另外还有一种placement new,它不会开辟新的内存空间,而是利用已有的内存空间,它有对应的operator delete,但这个operator delete什么也不做,程序员应该通过显示调用类的析构函数来销毁类对象,而不是’delete 类对象’:

class Test{public:    ...    // placement new    static void* operator new (size_t size, void* p)     {        return p; // 直接返回p就行了,p是已经存在的一块堆内存    }       // 不需要执行任何操作    static void operator delete(void*, void*)     {    }private:    int n_;};int main(void){    char chunk[10];    // 调用默认构造函数,同时类对象构建在chunk所占的内存块中(此处是在栈上)    // 如果不加(chunk)则调用的是operator new,此处调用的是placement new    Test *p = new (chunk) Test;     p->~Test(); // 不能 delete p; 只能显示调用析构函数    return 0;}

注:Test *p = new (chunk) Test;中的new其实是new operator,它包含两部分:new operator = placement new + 调用构造函数,同理Test *p = new Test;中的new也是new operator,它包含两部分:new operator = operator new + 调用构造函数

[补充]成员指针访问运算符 .*

可以参见这篇博客恼人的函数指针(二):指向类成员的指针。

原创粉丝点击