C++_面向对象的程序设计

面向对象(Object Oriented, OO)是当代计算软件开发方法;
对象:现实世界中的各种具体的或抽象的“事物”。

  • 概念

  类是具有相同属性和行为的一组对象的集合,它为属于该类的全部对象提供了统一的抽象描述,其内部包括属性和行为两个主要部分
    • 语法格式:

      • class 类名称
        {
          public://访问权限修饰符,公有成员(外部可以调用)
          protected://访问权限修饰符,保护成员(外部不可调用、派生类可以调用)
          private://访问权限修饰符私有成员(外部不可调用、派生类不可调用)
        };
    • 类对象

        类的对象是该类的某一特定实体,即类类型的变量。
        声明形式: 类名 对象名;
    • 类成员

      • 成员变量

        • class 类名
          {
             访问权限:
              类型  变量名;  //声明的时候不占用内存
          };
          注意:在定义成员变量时,不对成员变量赋初始值。
      • 成员函数

      可以写在类体重也可以写在类体外;
        • 成员函数写在类体中
          #pragma once
          #include <iostream>
          class Color
          {
          public:
              void setName(const char* n)
              {
                  strcpy_s(name, n);
              }
          private:
              char name[32];
          }
          

           

        • 成员函数写在类体外 
          Color.h
          #pragma once
          #include <iostream>
          class Color
          {
          public:
               void setName(const char* n);
           private:
               char name[32];
           };
           
           Color.cpp
           #include "stdafx.h"
           #include "Color.h" 
          void Color::setName(const char* n)
          {
               strcpy_s(name, n);
          }    
      • 类成员的访问

        (1)类中成员可以直接访问
            直接使用成员名。
        (2)类外访问
            对象使用“对象名.成员名”的方式访问public属性的成员。
             指针使用“指针变量->成员名”的方式访问public属性的成员。
        • #include "stdafx.h"
          #include <iostream>
          using namespace std;
          
          class Car//定义car类
          {
          public:
              void start()//成员函数
              {
                  cout << color << "" << name << "启动了" << endl;
          
              }
              void stop()//成员函数
              {
                  cout << color << "" << name << "停下了" << endl;
          
              }
              void set(const char * n, const char * c)//成员函数
              {
                  strcpy_s(name, n);
                  strcpy_s(color, c);
              }
          private:
              char name[32];//成员变量
              char color[32];//成员变量
          
          };
          
          int main()
          {
              Car a;//定义一个car类的对象a
              a.set("宝马", "红色");
              a.start();
              a.stop();
          
              Car b;
              b.set("奔驰", "蓝色");
              Car *p = &b;
              p->start();
              p->stop();
              return 0;
          }
  • 构造函数

    • 概念:

构造函数是实现数据成员初始化的特殊成员函数
    • 特点:

      • 与类同名,没有返回值;
      • 创建对象时,构造函数被自动调用。每创建一个对象都必须调用一次构造函数,每调用一次构造函数必定创建一个对象。
    • 语法格式:

      • 类中定义格式
          类名(形参列表)
          {…} //函数体,对数据成员赋值 类中声明,
      • 类外定义
          类中声明 类名(形参列表);
          类外定义 类名::类名(形参列表)
              {…} //函数体
    •  分类 

      • 缺省的构造函数

        :没有参数;
        • 系统自动产生的构造函数 类名() {}
          •  用户未定义构造函数时,系统会自动产生构造函数,用户一旦定义,系统就不会再产生构造函数。
        • 用户定义的无参构造函数
      • 普通构造函数

        :普通参数;
        • class String
           {
           public:
               String(char* p)  //有参构造函数
                {
                    memset(str, 0, sizeof(str));
                    cout << "用户定义有参构造函数" << endl;
            }
          }
          int main()
          {
              String s("Hello world!");
              s.output();
              return 0;
          }
      • 拷贝构造函数

        :参数为对象的引用。
        概念:拷贝构造函数是一种特殊的构造函数,其形参为本类的对象的引用
        • 拷贝构造函数的使用场景
          • 用于拿一个对象初始化另一个对象

          • 函数的形参是类的对象

          • 当对象作为值参数传递给函数时,会创建实参的副本,此时调用拷贝构造函数
          • class Person 
            {
            public:
            
                Person() {
                    cout << "Person默认构造函数调用" << endl;
                }
                Person(int age) {
                    cout << "Person有参构造函数调用" << endl;
                    m_Age = age;
                }
                Person(const Person &p) {
                    cout << "Person拷贝构造函数调用" << endl;
                    m_Age = p.m_Age;
                }
                ~Person() {
                    cout << "Person析构函数调用" << endl;
                }
                int m_Age;
            };
            //1使用一个已经创建完毕的对象来初始化一个新对象 void test01() { Person p1(20); Person p2(p1); cout << "P2年龄为:"<<p2.m_Age<< endl; }
            //2值传递的方式给函数参数传值void doWork(Person p) { } void test02() { Person p; doWork(p); }
            //3以值方式返回局部对象 Person doWork2() { Person p1; cout << (int*)&p1<< endl; return p1; } void test03() { Person p=doWork2(); cout << (int*)&p << endl; } void main() { test01(); test02(); test03(); }

             

        • 注意
          • 默认拷贝构造函数可以完成对象的数据成员简单的复制,实际上默认的拷贝构造函数只是将指针复制。
          • 在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
        • 浅拷贝:类成员变量中有指针,文件、句柄等情况下,浅拷贝就有可能让类无法正常工作。
  原因:以指针为例,拷贝只是复制了指针变量的地址,而其内存数据未拷贝,一旦s1释放内存,等到s2释放内存时,已找不到指针所指向的内存,此时为野指针,带来的后果具有不可预测性,程序执行到此会崩溃,
                         
        • 深拷贝:让新产生的对象,其成员变量对资源的引用操持独立,相互之间不受影响,仅保持“值”相同。
                 
          • 深拷贝示例:

            #include "stdafx.h" #include <iostream> using namespace std; class Student { private: int num; char *name; public: Student(); ~Student(); Student(const Student &s);//拷贝构造函数,const防止对象被改变 }; Student::Student() { name = new char(20); cout << "Student" << endl; } Student::~Student() { cout << "~Student " << (int)name << endl; delete name; name = NULL; } Student::Student(const Student &s) { name = new char(20); memcpy(name, s.name, strlen(s.name)); cout << "copy Student" << endl; } int main() { Student s1; Student s2(s1);// 复制对象return 0; }
  • 析构函数

    • 概念

用于撤销对象的成员函数造成的垃圾,比如开辟的空间(对象释放前系统自动调用)。、 
    • 语法格式

      • 1)类中定义
          ~类名()
          {…} //函数体
        2) 类中说明,类外定义
          Α  类中声明
          ~类名(); //类中声明
          Β   类外定义
          类名::~类名()
          {…} //函数体
    • 注意事项:

    (1)析构函数的名称由运算符“~”与类名组成;
    (2)析构函数无参数,无返回值;
    (3)析构函数不可以重载,即一个类只有一个析构函数;
    (4)如果用户没有定义,系统会自动生成默认析构函数: 类名::~类名() {}
    (5)当类中用new运算符分配了动态空间时,必须定义析构函数,并在函数体中用delete运算符释放动态存储空间。
    (6)析构函数调用与构造函数相反,先构造的后析构,后构造的先析构;
      • #include "stdafx.h"
        #include <iostream>
        #include <string.h>
        using namespace std;
        class String
        {
        public:
            String(const char *str)
            {
                int len = strlen(str) + 1;
                p = new char[len];
                strcpy_s(p, len, str);
                cout << "创建对象" << endl;
            }
        
            void output()
            {
                cout << p << endl;
                cout << "打印对象" << endl;
            }
        
            ~String()
            {
                if (p != nullptr)
                {
                    delete [ ] p;
                    p = nullptr;
                    cout << "对象析构" << endl;
                }
            }
        
        private:
            char *p;
        };
        
        int main()
        {
            String s("hello world");
            s.output();
            return 0;
  • 成员变量和成员函数

    • 静态成员

      •  静态成员变量

        • #include "stdafx.h"
          #include <iostream>
          using namespace std;
          
          class Point
          {
              int x;
              int y;
          
          public:
              static int no;//声明静态成员变量
              Point(int a, int b) :x(a), y(b)
              {
                  ++no;
              }
          };
          
          int  Point::no = 0;//必须在类体外初始化静态成员变量
          
          void  main()
          {
              Point p(1,10);
              Point p1(2,4);
              cout << Point::no << endl;//调用静态成员变量
          }
      • 说明:

        • 静态成员变量不属于某个对象,而是属于类,它由某个类的所有对象共有。
        • 静态成员变量的定义 静态成员变量定义后,必须对它进行初始化,并且一定要在类外进行。
        • 静态成员变量只能够初始化一次
        • 初始化的形式如下: 类型 类名::静态成员变量 = 表达式; 初始化时不加关键字static 
    • 静态成员函数

    • 说明:
      • 静态成员函数也从属于类,由同一个类的所有对象共同拥有。
      • 静态成员函数只能直接访问该类的静态成员变量、静态成员函数以及类以外的数据和函数,而访问非静态成员必须通过参数传递方式得到类的对象,然后通过对象名来访问。
      • 在类内可以直接通过函数名称访问。
      • 在类外可以通过 类名::静态成员函数(实参)、对象.静态成员函数(实参)、指针->静态成员函数(实参) 来访问。
      • class Point
        {
            int x, y;
            static int no;    //声明静态成员变量
        public:
            Point(int a, int b) : x(a), y(b) { ++no; }
            static void print() {     //静态成员函数
                std::cout << “no=” << no << std::endl;
            }
        };
        int Point::no = 0; 
        void main(){
            Point pt(1, 10);
            Point* p1 = new Point(3, 3);
            pt.print();       //对象名.静态成员函数名()
            p1->print();    //指针->静态成员函数名()
            Point::print(); //类名::静态成员函数名()
        }
  • 类的对象的大小以及在内存中类的对齐规则

    • 类和对象的大小就是 :成员变量+虚表指针。
    • 类(结构体)在内存对齐规则:
      • 第一个成员在结构体变量偏移量为0的地址处。
      • 其他成员要对齐到对齐数的整数倍地址处。
      • 结构体总大小为最大对齐数的整数倍。 最大对齐数为所有成员变量的对齐数的最大值。
    • 我们已经知道结构体在内存中需要对齐,那么为什么需要对齐呢?
      • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
      • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(提高CPU处理速度
  • this 指针

C++编译器给每个“成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。当我们使用对象来调用类中的成员函数时,会将对象的地址传给 this 指针,也就是说 this 指针指向的就是调用成员函数的对象。

    • this指针的类型:类类型* const
    • 只能在“成员函数”的内部使用
    • this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
    • this指针是成员函数第一个隐含的指针形参
  • explicit 关键字

    在 C++ 中,单参数构造函数(或者是除了第一个参数,其余参数都有默认值的多参构造函数)有两个作用:一是初始化对象,二是隐式类型转换

    • 背景
      • class A
        {
        public:
            A(int _a)
            {
                a = _a;
            }
            void print()
            {
                cout << a << endl;
            }
        private:
            int a;
        };
         
        int main()
        {
            A obj(5);
            obj = 10;
            obj.print();
         
            return 0;
        }

        上述代码相当于用一个整型 10 给obj 赋值。编译器会用 10 构建一个无名对象,然后用这个无名对象给 obj 赋值。
        但是这样的代码不具有可读性,所有引入了 explicit 关键字。用 explicit 修饰构造函数,将会禁止单参构造函数的隐式转换

      • class A
        {
        public:
            explicit A(int _a)
            {
                a = _a;
            }
            void print()
            {
                cout << a << endl;
            }
        private:
            int a;
         
        };
         
        int main()
        {
            A obj(5);
            obj = 10;
            obj.print();
         
            return 0;
        }
  • 类的三大特性

    • 封装

      • 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
      • 有效地做到了信息隐藏,提高程序的安全性,并且类内部的改动不会影响外部的使用,提高了程序的可维护性。
    • 继承

      • 通过子类继承父类已有的特性,在无需重复实现同样功能的前提下扩展自身的新功能, 解决了代码复用的问题,这种模式大大提高了开发效率
      • 菱形继承会出现数据冗余和二义性的问题。解决菱形继承的问题引入virtual关键字,这样在类A和B中只保存了偏移地址,个人理解应该是有虚函数指针,这样将Base中数据保存至一个公共区域,这样解决了数据冗余和二义性的问题。
    • 多态

      • 概念:

具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态.

对于多态来说,首先要理解重载和覆盖(重写)的区别。

  覆盖(重写):在子类继承父类中,前提是父类中定义了虚函数,若子类中对虚函数进行重新实现,则在子类虚函数表中该虚函数地址改为修改后的虚函数地址。一般需使用virtual关键字,其强调在不同作用域下的子类和父类。

  重定义:基类和派生类中的两个同名函数不构成重写就是重定义,重写是一种特殊的重定义

      • 静态多态

静态多态主要就是函数重载和函数模板

        • 运算符重载
          • 概念
运算符重载:就是对已有的运算符重新进行定义,赋予一种功能,以适应不同的数据类型。
运算符只能运算内置的数据类型,对于自定义的数据类型,不能运算,所以 我们可以重载运算符
          • 语法
定义重载的运算符就像定义函数,只是函数的名字为operator+重载的符号。比如重载+,函数名就是operator+。
返回类型 operator 运算符(参数列表) { 函数体; }
          • 原则
            • 被重载的运算符不改变原来的操作数个数、优先级和结合性。
            • 不能创新发明。
            • 不能改变运算符对预定义类型(基本类型)的操作方式。
            • 成员函数实现运算符重载时,运算符的左操作数为当前对象。
            • 如果重载的运算符函数是双目的运算符,则参数表中有一个参数,若为单目运算符,则参数表中有零个参数(没有参数),特殊情况除外,比如++、--运算符。
          • 不可重载的运算符:
    
          • #include <iostream>
            #include <string>
            using namespace std;
            
            class Person
            {
            public:
                Person(int age)
                {
                    m_Age = age;
                }
                int m_Age;
            };
            //重载加运算符
            Person operator+(Person &p1, Person &p2)
            {
                Person p(p1.m_Age + p2.m_Age);
                return p;
            }
            void test01()
            {
                Person p1(10);
                Person p2(20);
                Person p3 = p1 + p2;// operator+(p1,p2)
                cout << p3.m_Age << endl;
            }
            int main()
            {
                test01();//结果为30
                return 0;
            }

             

        • 函数重载

在C++中,函数重载(Function Overloading)是一种允许在相同作用域内定义多个具有不同参数列表的同名函数的能力。编译器会根据函数的参数类型和数量来区分这些函数。这使得你可以使用相同的函数名来执行不同的操作,只要它们的参数列表不同

          • #include "stdafx.h"
            #include <iostream>
            using namespace std;
            
            int  add(int a, int b)
            {
                return a + b;
            }
            
            double  add(double a, double b)
            {
                return a + b;
            }
            
            const char*  add(char *a,char *b)
            {
                strcat_s(a,100,b);
                return a;
            }
            
            int main()
            {
                char s[100] = {"hello "};
                cout << add(10, 10) << endl;
                cout << add(10.9, 10.5) << endl;
                cout << add(s, "world") << endl;
                return 0;
            }
        • 函数模版
          • 作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
          • 定义:
            #include<iostream>
            using namespace std;
            //利用模板提供通用的交换函数
            template<typename T>
            void mySwap(T& a, T& b)
            {
                T temp = a;
                a = b;
                b = temp;
            }
            int main()
            {
                int a =10;
                int b =20;
                //swapInt(a, b);
             
                //利用模板实现交换
                //1、自动类型推导
                mySwap(a, b);
             
                //2、显示指定类型
                mySwap<int>(a, b);
                cout << "a= "<< a<<endl;
                cout << "b= "<< b<<endl;
                return 0;
            }
          • 模版分为

            • 自动类型推导(编译器给你识别T的类型)

            • 手动类型,mySwap<int>(a, b);

          • 如果函数模板和普通函数都可以实现,优先调用普通函数

          • 可以通过空模板参数列表来强制调用函数模板

                myPrint<>(a, b); //调用函数模板

      • 动态多态

        • class Person
          {
          public:
              virtual void BuyTicket()
              {
                  cout << "全价买票" << endl;
              }
          };
           
          class Student : public Person {
          public:
              void BuyTicket()
              {
                  cout  << "打折买票" << endl;
              }
          };
           
          void Func(Person* ptr)
          {
              ptr->BuyTicket();
          }
           
          int main()
          {
              Person ps;
              Student st;
              
              Func(&ps);
              Func(&st);
           
              return 0;
          }
        • 实现多态的条件
          • 必须是父类的指针或者引用。

因为只有是父类的指针或者引用才可以既能指向父类对象,又能指向子类对象。

          • 子类必须对父类的虚函数进行重写/覆盖。
        • 虚函数表
          • 一个含有虚函数的类中至少有一个虚函数表指针,一个类所有的虚函数的地址都要放到这个类对象的虚函数表中,虚函数表也叫做虚表。

          • 同一类类型的对象共享同一张虚函数表,不同类类型的对象的虚函数表则不一样,具体点就是不同类对象的虚函数表指针不一样。
          • 基类对象的虚函数表中存放基类所有的虚函数的地址。
          • 派生类重写的基类的虚函数,派生类的虚函数表中对应的虚函数被覆盖成派生类重写的虚函数地址。
          • 派生类的虚函数表包含,基类的虚函数地址、派生类重写的虚函数地址、派生类自己的虚函数地址三部分。
          • 虚函数表本质是一个函数指针数组
  • 动态绑定与静态绑定

    对不满足多态条件(指针+引用调用虚函数)的函数调用是在编译时绑定调用函数的地址,叫做静态绑定。

    满足多态条件的函数调用是在运行时绑定函数调用的地址,也就是运行时到指向对象的虚函数表中找到调用函数的地址,叫做动态绑定。

  • override和final关键字

    • override只能修饰子类的虚函数(放在函数参数列表的后面),用于检测该虚函数是否完成重写,如果没有,则报错(编译错误)
    • final:如果不想让子类重写父类虚函数,则我们用final修饰。如果final修饰了一个类,那这个类就不能被其他类继承。
  • 抽象类

在虚函数后面加上 =0,这个虚函数就是纯虚函数。纯虚函数不需要定义,只要需要声明就行了。

含有纯虚函数的类叫做抽象类,抽象类不能实例化出对象。当子类继承抽象类后不重写虚函数那么子类也是抽象类。所以纯虚函数在某种程度上强制要求子类重写虚函数,因为不重写子类实例不出具体的对象。

  • 友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
明,声明时需要加friend关键字。

    • 友元函数可访问类的私有成员,但不是类的成员函数
    • 友元函数不能用const修饰
    • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
    •  一个函数可以是多个类的友元函数
    • 友元函数的调用与普通函数的调用和原理相同
    • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
      • class Date; // 前置声明
        class Time
        {
           friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变public:
           Time(int hour, int minute, int second): _hour(hour), _minute(minute), _second(second)
           {}
         private:
           int _hour;
           int _minute;
           int _second;
        };
        class Date
        {
        public:
           Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day)
           {}
          void SetTimeOfDate(int hour, int minute, int second)
           {
             // 直接访问时间类私有的成员变量
             _t._hour = hour;
             _t._minute = minute;
             _t.second = second;
           }
         
        private:
         int _year;
         int _month;
         int _day;
         Time _t;
        };
      • 在一个类中声明另一类为该类的友元类时,必须要前置声明该友元类。
      • 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
      • 友元关系不能传递如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  • const成员函数

    • 用const修饰的成员函数称为const成员函数。const修饰成员函数实际上是修饰成员函数隐含的this指针,表明在该成员函数中,不能对调用该成员函数的对象做任何修改。
    •  const对象不可以调用非const成员函数,非const对象可以调用const成员函数;const成员函数内部不可以调用非const成员函数,非const成员函数内部可以调用const成员函数。
    • image

  • 常见问题

    • 1. 什么是多态?
      答:多态分为静态多态:函数重载;和动态多态:继承中的虚函数重写+基类指针引用。


      2. 什么是重载、重写 ( 覆盖 ) 、重定义 ( 隐藏 ) ?
      答:参考上述内容。


      3. 多态的实现原理?
      答:静态多态:函数名修饰规则;动态多态:虚函数表。


      4. inline 函数可以是虚函数吗?
      答:可以,不过编译器就忽略 inline 属性,这个函数就不再是inline,因为虚函数要放到虚表中去。


      5. 静态成员可以是虚函数吗?
      答:不能。静态成员函数属于类本身,而不是类的某个特定对象。它可以通过类名直接调用,不需要类的实例。虚函数是为了实现多态性,允许在运行时根据对象的实际类型调用相应的函数。虚函数需要依赖于对象的动态类型,而静态成员函数不依赖于任何对象的类型。两者的这种特性有所冲突,所以禁止将静态成员函数声明为虚函数。

      6. 构造函数可以是虚函数吗?
      答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。


      7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
      答:可以,并且最好把基类的析构函数定义成虚函数。参考上述内容。


      8. 对象访问普通函数快还是虚函数更快?
      答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

      9. 虚函数表是在什么阶段生成的,存在哪的?
      答:虚函数表是在编译阶段就生成的,一般情况下存在代码段( 常量区 ) 的。

      10. C++ 菱形继承的问题?虚继承的原理?
      答:参考继承。注意这里不要把虚函数表和虚基表搞混了。

      11. 什么是抽象类?抽象类的作用?

posted @ 2020-03-21 00:16  直至成伤  阅读(352)  评论(0)    收藏  举报