美文网首页
C++ — 类 & 对象超详解

C++ — 类 & 对象超详解

作者: 苏州韭菜明 | 来源:发表于2019-01-31 13:40 被阅读153次
    20190130164942.png
    文中示例代码gayhub传送门

    C++ 类 & 对象

    C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。

    类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。

    C++ 类定义

    定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。

    类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表。例如,我们使用关键字 class 定义 Person数据类型,如下所示:

    class Person {
    public:
        int age;
        char name;
        char sex;
        int id;
    };
    

    关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。您也可以指定类的成员为 privateprotected,这个我们稍后会进行讲解。

    定义 C++ 对象

    类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。下面的语句声明了类 Person的两个对象:

    Person zhang;
    Person li;
    

    访问数据成员

    类的对象的公共数据成员可以使用直接成员访问运算符 (.) 来访问。为了更好地理解这些概念,让我们尝试一下下面的实例:

    
    void set_print_person() {
        Person zhang;
        Person li;
        zhang.age = 19;
        zhang.id = 13952;
        zhang.name = 'i';
        zhang.sex = 'g';
        li.name = 'j';
        li.age = 19;
        li.id = 4538;
    
        li.sex = 'b';
        cout << "zhang.age is " << zhang.age << "  li.id is " << li.id << endl;
        cout << "zhang.name is " << zhang.name << "  li.name is " << li.name << endl;
    }
    

    类 & 对象详解

    到目前为止,我们已经对 C++ 的类和对象有了基本的了解。下面的列表中还列出了其他一些 C++ 类和对象相关的概念,可以点击相应的链接进行学习。

    概念 描述
    [类成员函数] 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。
    [类访问修饰符] 类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。
    [构造函数 & 析构函数] 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。
    [C++ 拷贝构造函数] 拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
    [C++ 友元函数] 友元函数可以访问类的 private 和 protected 成员。
    [C++ 内联函数] 通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。
    [C++ 中的 this 指针] 每个对象都有一个特殊的指针 this,它指向对象本身。
    [C++ 中指向类的指针] 指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。
    [C++ 类的静态成员] 类的数据成员和函数成员都可以被声明为静态的。

    C++ 类成员函数

    类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。

    成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。所以您可以按照如下方式定义 Volume() 函数:

    class Box {
    public:
        double length;         // 长度
        double breadth;        // 宽度
        double height;         // 高度
    
        double getVolume(void)  // 返回体积
        {
            return length * breadth * height;
        }
    };
    

    您也可以在类的外部使用范围解析运算符 :: 定义该函数,如下所示:

    double Box::getVolume(void) 
    {     return length * breadth * height; }
    

    在这里,需要强调一点,在 :: 运算符之前必须使用类名。调用成员函数是在对象上使用点运算符(.),这样它就能操作与该对象相关的数据,如下所示:

    Box myBox;          // 创建一个对象
    myBox.getVolume();  // 调用该对象的成员函数
    

    让我们使用上面提到的概念来设置和获取类中不同的成员的值:

    class Box {//和Java类相似
    public:
        double length;         // 长度
        double breadth;        // 宽度
        double height;         // 高度
    
        double getVolume(void)  // 返回体积
        {
            return length * breadth * height;
        }
    
        double getLength() const {
            return length;
        }
    
        void setLength(double length) {
            Box::length = length;
        }
    
        double getBreadth() const {
            return breadth;
        }
    
        void setBreadth(double breadth) {
            Box::breadth = breadth;
        }
    
        double getHeight() const {
            return height;
        }
    
        void setHeight(double height) {
            Box::height = height;
        }
    };
    

    如何为变量添加get和set方法呢?

    JetBrains CLion 有快捷键如下图

    20190130145524.png 20190130145554.png 20190130145609.png

    则会自动生成上图

    void  set_print_boxes(){
        Box Box1;                // 声明 Box1,类型为 Box
        Box Box2;                // 声明 Box2,类型为 Box
        double volume = 0.0;     // 用于存储体积
    
        // box 1 详述
        Box1.setLength(6.0);
        Box1.setBreadth(7.0);
        Box1.setHeight(5.0);
    
        // box 2 详述
        Box2.setLength(12.0);
        Box2.setBreadth(13.0);
        Box2.setHeight(10.0);
    
        // box 1 的体积
        volume = Box1.getVolume();
        cout << "Box1 getVolume is :" << volume <<endl;
    
        // box 2 的体积
        volume = Box2.getVolume();
        cout << "Box2 getVolume is :" << volume <<endl;
    }
    

    :: 叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。

    :: 可以不跟类名,表示全局数据或全局函数(即非成员函数)。

    C++ 类访问修饰符

    数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。

    一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。

    
    class Base {
     
       public:
     
      // 公有成员
     
       protected:
     
      // 受保护成员
     
       private:
     
      // 私有成员
     
    };
    

    公有(public)成员

    公有成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值,如下所示:

    私有(private)成员

    私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。

    默认情况下,类的所有成员都是私有的。例如在下面的类中,width 是一个私有成员,这意味着,如果您没有使用任何访问修饰符,类的成员将被假定为私有成员:

    保护(protected)成员

    保护成员变量或函数与私有成员十分相似,但有一点不同,保护成员在派生类(即子类)中是可访问的。

    在下一个章节中,您将学习到派生类和继承的知识。现在您可以看到下面的实例中,我们从父类 Student派生了一个子类 LittleStudent

    下面的实例与前面的实例类似,在这里 **address成员可被派生类 Student的任何成员函数访问。

    class Student {
    private:
        int grade;
    private:
        char classroom;
    public:
        char name;
    protected:
        char address;
    
    public:
        char getClassroom() const {
            return classroom;
        }
    
        void setClassroom(char classroom) {
            Student::classroom = classroom;
        }
    };
    
    class LittleStudent : Student {
    
    public:
        void setAddress(char addrs) {
            address = addrs;
        }
    
        char getsetAddress() {
            return address;
        }
    };
    
    void set_print_student(){
        Student student;
        student.name='Y';
        cout << "student name is :" << student.name <<endl;
        student.setClassroom('5');
        cout << "student getClassroom is :" << student.getClassroom() <<endl;
        LittleStudent littleStudent;
        littleStudent.setAddress('N');
        cout << "student getAddress is :" << littleStudent.getAddress() <<endl;
    }
    

    当上面的代码被编译和执行时,它会产生下列结果:

    student name is :Y
    student getClassroom is :5
    student getAddress is :N
    

    继承中的特点

    有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。

    • 1.public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
    • 2.protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
    • 3.private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private

    但无论哪种继承方式,上面两点都没有改变:

    • 1.private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
    • 2.protected 成员可以被派生类访问。

    如果继承时不显示声明是 private,protected,public 继承,则默认是 private 继承,在 struct 中默认 public 继承

    class B : A {};
    B b;
    b.a;    //错误
    b.a1;   //错误
    b.a2;   //错误
    b.a3;   //错误
    

    总结一下三种继承方式:

    继承方式 基类的public成员 基类的protected成员 基类的private成员 继承引起的访问控制关系变化概括
    public继承 仍为public成员 仍为protected成员 不可见 基类的非私有成员在子类的访问属性不变
    protected继承 变为protected成员 变为protected成员 不可见 基类的非私有成员都为子类的保护成员
    private继承 变为private成员 变为private成员 不可见 基类中的非私有成员都称为子类的私有成员

    C++ 类构造函数 & 析构函数

    类的构造函数

    类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

    构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

    下面的实例有助于更好地理解构造函数的概念:

    class Person {//不推荐将get和set以及构造函数写在class内,但我这么做,为了更好演示理解
    public:
        int age;
        char name;
        char sex;
        int id;
        Person(){//无参构造函数
            cout << "Person is being created" <<endl;
        };
        Person(int age, int id) : age(age), id(id) {//带参数构造,使用初始化列表来初始化字段
            cout << "Person is being created with param" << endl;
            cout << "Person age is :" << Person::age << endl;
            cout << "Person  id is :" << Person::id << endl;
        }    
    };
    

    测试函数如下:

    void set_print_person() {
        Person zhang;
        Person li(19,29);
        zhang.age = 19;
        zhang.id = 13952;
        zhang.name = 'i';
        zhang.sex = 'g';
        li.name = 'j';
        li.sex = 'b';
        cout << "zhang.age is " << zhang.age << "  zhang.name is " << zhang.name << endl;
        cout << "li.age is " << li.age << "  li.id is " << li.id << endl;
    }
    

    打印结果如下:

    Person is being created without param
    Person is being created with param
    Person age is :19
    Person  id is :29
    zhang.age is 19  zhang.name is i
    li.age is 19  li.id is 29
    

    类的析构函数

    类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。

    析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

    下面的实例有助于更好地理解析构函数的概念:

    class Person {
    public:
        int age;
        char name;
        char sex;
        int id;
    
        Person() {
            cout << "Person is being created without param" << endl;
        };
        ~Person(){
            cout << "Person is being deleted " << endl;
        };  // 这是析构函数声明
    
        Person(int age, int id) : age(age), id(id) {
            cout << "Person is being created with params" << endl;
            cout << "Person age is :" << Person::age << endl;
            cout << "Person  id is :" << Person::id << endl;
        }
    };
    

    测试代码如上,不做改变,打印如下:

    Person is being created without param
    Person is being created with params
    Person age is :19
    Person  id is :29
    zhang.age is 19  zhang.name is i
    li.age is 19  li.id is 29
    Person is being deleted
    Person is being deleted
    

    C++ 拷贝构造函数

    拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

    • 通过使用另一个同类型的对象来初始化新创建的对象。
    • 复制对象把它作为参数传递给函数。
    • 复制对象,并从函数返回这个对象。

    如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:

    
    classname (const classname &obj) {
       // 构造函数的主体
    }
    
    

    在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。

    class AllLife {//此处有些问题,待请教
    private:
        int *ptr;
    public:
        AllLife(int len) {//构造函数
            cout << "using alllife  constructor" << endl;
            // 为指针分配内存
            ptr = new int;
            *ptr = len;
        };
    
        AllLife(const AllLife &obj) {
            cout << "using alllife copy constructor and location mem for pointer  ptr " << endl;
            ptr = new int;
            *ptr = *obj.ptr; // 拷贝值
        }
    
        ~AllLife() {//析构函数
            cout << "release ptr memory" << endl;
            delete (ptr);
    
        }
    
        int getLength() {
            return *ptr;
        }
    
        void printLength() {
            cout << "AllLife size is " << getLength() << endl;
        }
    };
    

    测试代码如下:

    void set_print_alllife(){
        AllLife allLife(100);
        allLife.printLength();
        AllLife allLif=allLif;
        allLif.printLength();
    }
    
    

    打印结果如下:

    using alllife  constructor
    AllLife size is 100
    using alllife copy constructor and location mem for pointer  ptr
    AllLife size is 1906976
    release ptr memory
    release ptr memory
    

    拷贝构造函数几个原则:

    C++ primer p406 :拷贝构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用拷贝构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用拷贝构造函数。

    C++支持两种初始化形式:

    拷贝初始化 int a = 5; 和直接初始化 int a(5); 对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数,也就是说:

    A x(2);  //直接初始化,调用构造函数
    A y = x;  //拷贝初始化,调用拷贝构造函数
    

    必须定义拷贝构造函数的情况:

    只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义拷贝构造函数也可以拷贝;有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义拷贝构造函数。

    什么情况使用拷贝构造函数:

    类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

    • (1)一个对象以值传递的方式传入函数体

    • (2)一个对象以值传递的方式从函数返回

    • (3)一个对象需要通过另外一个对象进行初始化。

    C++ 友元函数

    类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

    友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

    如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:

    class Box
    {
       double width;
    public:
       double length;
       friend void printWidth( Box box );
       void setWidth( double wid );
    };
    

    若声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:

    class ClassOne 
    {
    friend class ClassTwo;
    };
    

    请看下面的程序:

    class Box {
    public:
        double length;         // 长度
        double breadth;        // 宽度
        double height;         // 高度
    
        friend void printBreadth(Box box){// 请注意:printWidth() 不是任何类的成员函数
            /** 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
            cout << "breadth of box : " << box.breadth <<endl;
        }
        double getVolume(void)  // 返回体积
        {
            return length * breadth * height;
        }
    
        double getLength() const {
            return length;
        }
    
        void setLength(double length) {
            Box::length = length;
        }
    
        double getBreadth() const {
            return breadth;
        }
    
        void setBreadth(double breadth) {
            Box::breadth = breadth;
        }
    
        double getHeight() const {
            return height;
        }
    
        void setHeight(double height) {
            Box::height = height;
        }
    };
    
    void  set_print_boxes(){
        Box Box1;                // 声明 Box1,类型为 Box
        Box Box2;                // 声明 Box2,类型为 Box
        double volume = 0.0;     // 用于存储体积
    
        // box 1 详述
        Box1.setLength(6.0);
        Box1.setBreadth(7.0);
        Box1.setHeight(5.0);
    
        // box 2 详述
        Box2.setLength(12.0);
        Box2.setBreadth(13.0);
        Box2.setHeight(10.0);
    
        // box 1 的体积
        volume = Box1.getVolume();
        cout << "Box1 getVolume is :" << volume <<endl;
        printBreadth(Box1);// 使用友元函数输出Breadth
    
        // box 2 的体积
        volume = Box2.getVolume();
        cout << "Box2 getVolume is :" << volume <<endl;
    }
    

    友元函数的使用

    因为友元函数没有this指针,则参数要有三种情况:

    • 要访问非static成员时,需要对象做参数;

    • 要访问static成员或全局变量时,则不需要对象做参数;

    • 如果做参数的对象是全局对象,则不需要对象做参数.

    • 可以直接调用友元函数,不需要通过对象或指针

    C++ 内联函数

    C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。

    对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。

    如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。

    在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

    下面是一个实例,使用内联函数来返回两个数中的最大值:

    #include <iostream>
     
    using std::cout;
    using std::endl;
    
    inline int Max(int x, int y)
    {
       return (x > y)? x : y;
    }
    
    // 程序的主函数
    int main( )
    {
    
       cout << "Max (20,10): " << Max(20,10) << endl;
       cout << "Max (0,200): " << Max(0,200) << endl;
       cout << "Max (100,1010): " << Max(100,1010) << endl;
       return 0;
    }
    

    内联函数inline:引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:

    • 1.在内联函数内不允许使用循环语句和开关语句;
    • 2.内联函数的定义必须出现在内联函数第一次调用之前;
    • 3.类结构中所在的类说明内部定义的函数是内联函数。

    C++ this 指针

    在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。

    友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。

    C++ 指向类的指针

    一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。

    下面的实例有助于更好地理解指向类的指针的概念:

    class Box {
    public:
        double length;         // 长度
        double breadth;        // 宽度
        double height;         // 高度
    
        friend void printBreadth(Box box){// 请注意:printWidth() 不是任何类的成员函数
            /** 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
            cout << "breadth of box : " << box.breadth <<endl;
        }
        int compare(Box box){//这里是与上方代码的不同之处,this 指针,下方代码省略
            return this->getVolume()>box.getVolume();
        }
    };
    
    void  set_print_boxes(){
        Box Box1;                // 声明 Box1,类型为 Box
        Box Box2;                // 声明 Box2,类型为 Box
        double volume = 0.0;     // 用于存储体积
        Box *ptrBox;                // Declare pointer to a class. C++ 指向类的指针
        
    
        // box 1 详述
        Box1.setLength(6.0);
        Box1.setBreadth(7.0);
        Box1.setHeight(5.0);
    
        // box 2 详述
        Box2.setLength(12.0);
        Box2.setBreadth(13.0);
        Box2.setHeight(10.0);
        
        ptrBox = &Box1;// 保存第一个对象的地址
        // 现在尝试使用成员访问运算符来访问成员
        // C++ 指向类的指针
        cout << "ptrBox getVolume is :" << ptrBox->getVolume() <<endl;
    
        // box 1 的体积
        volume = Box1.getVolume();
        cout << "Box1 getVolume is :" << volume <<endl;
        printBreadth(Box1);
    
        // box 2 的体积
        volume = Box2.getVolume();
        cout << "Box2 getVolume is :" << volume <<endl;
        if(Box1.compare(Box2))//与上方代码不同之处
        {
            cout << "Box2 is smaller than Box1" <<endl;
        }
        else
        {
            cout << "Box2 is equal to or larger than Box1" <<endl;
        }
    
    }
    

    C++ 类的静态成员

    (比较简单,不做实例说明)

    我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。

    静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。

    静态成员函数

    如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。

    静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。

    静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。

    静态成员函数与普通成员函数的区别:

    • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
    • 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。

    相关文章

      网友评论

          本文标题:C++ — 类 & 对象超详解

          本文链接:https://www.haomeiwen.com/subject/zrvksqtx.html