C++06面向对象

前言:更多内容请看总纲《嵌入式C/C++学习路》

1. 什么是面向对象?

  •  万物皆对象,这是人类面对世界最朴素,最自然的感觉、想法和观点
  • 把大型软件看成是一个由对象组成的社会
  • 对象拥有足够的智能,能够理解来自其他对象的信息,并以适当的行为作出反应
  • 对象能够从高层对象继承属性和行为,并允许低层对象从自己继承属性和行为等
  • 编写程序的过程就是描述对象属性和行为的过程,凭借这种能力使问题域解域获得最大程度的统一
  • 面向对象的三大要件:封装、继承和多态

2. 怎样面向对象?

  • 至少掌握一种面向对象的程序设计语言,如C++
  • 深入理解封装、继承和多态等面向对象的重要概念
  • 精通一种元语言,如UML,在概念层次上描述设计。UML类图如下所示
  • 学习设计模式,源自多年成功经验的积累和总结
UML类图

 3. 类和对象

  • 拥有相同属性行为的对象被分成一组,即一个类
  • 类可用于表达那些不能直接与内置类型建立自然映射关系的逻辑抽象
  • 类是一种用户自定义的复合数据类型,即包括表达属性的成员变量,也包括表达行为的成员函数
  • 类是显示世界的抽象,对象是类在虚拟世界的实例
类和对象

类的定义与实例化

类的一般形式

访问控制限定符

  • 在C++中,类和结构已经没有本质型的差别,唯一的不同在于:类class的缺省访问控制属性为私有结构struct的缺省访问控制属性为公有
  • 访问控制限定符仅作用于,而非作用于对象,因此同一个类的不同对象,可以互相访问非公有部分
  • 对不同成员的访问控制属性加以区分,体现了C++作为面向对象程序设计语言的封装特性

访问控制限定符

 举个简单的例子,假如基类是我,子类是我的老婆儿子,外部是我的同事:

  • 公有成员是我的名字,不管是我的家人还是同事朋友都知道
  • 保护成员是我的小名,只有我和老婆儿子知道,同事不知道
  • 私有成员是我的私房钱,只有我自己知道,老婆儿子同事都不知道

下面通过一段代码学习一下:

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public:            // 公有成员访问控制符
    void who(void) // 行为   成员函数
    {
        cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
    }
    void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改
    {
        cout << "我在学习" << course << "课程。" << endl;
    }
    string m_name; // 成员变量
    int m_age;
};

// 把学生类变成对象(实例化)
int main(void)
{
    Student s1 = {"张三", 22}; // 初始化对象
    s1.who();                  // 调用成员函数
    s1.learn("C++");
    s1.m_name = "二货";    // 用户可以随意修改公有成员变量
    s1.m_age = -100;
    s1.who();                  // 调用成员函数
}

从输出可以看到,用户可以随意修改公有成员变量,所以出现了私有成员,将名字和年龄放入私有成员以后,再次编译代码将会报错: 

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public:            // 公有成员访问控制符
    void who(void) // 行为   成员函数
    {
        cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
    }
    void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改
    {
        cout << "我在学习" << course << "课程。" << endl;
    }
private:  // 私有成员控制访问符
    string m_name; // 成员变量
    int m_age;
};

// 把学生类变成对象(实例化)
int main(void)
{
    Student s1 = {"张三", 22}; // 初始化对象
    s1.who();                  // 调用成员函数
    s1.learn("C++");
    s1.m_name = "二货";    // 用户可以随意修改公有成员变量
    s1.m_age = -100;
    s1.who();                  // 调用成员函数
}

 

 将名字放入私有成员后,用户该怎么给学生对象命名呢?可以在公有成员里面创建一个函数,当学生姓名/年龄满足一定条件的时候,才创建学生对象。因为类的内部可以访问私有成员变量。

例子如下:假如用户给学生命名为”二货“或者年龄 <=0 的时候,拒绝创建。

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public:            // 公有成员访问控制符
    void who(void) // 行为   成员函数
    {
        cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
    }
    void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改
    {
        cout << "我在学习" << course << "课程。" << endl;
    }
    void setName(const string& name){
        if(name == "二货"){
            cout << "你才是二货!!!" << endl;
            return ;
        }
        m_name = name;
    }
    void setAge(int age){
        if(age <= 0){
            cout << "非法年龄!" << endl;
            return ;
        }
        m_age = age;
    }
private:
    string m_name; // 成员变量
    int m_age;
};

// 把学生类变成对象(实例化)
int main(void)
{
    Student s1; // 初始化对象
    s1.setName("二货");
    s1.setAge(-50);
    s1.who();  // 调用成员函数
    s1.setName("张飞");
    s1.setAge(22);
    s1.who();  // 调用成员函数
}

 可以看到输入的名字和年龄不满足条件时,s1就是我们初次定义的对象即Student s1;,这里名字是字符串‘\0’,age是随机的数字(这里其实跟代码有关,我们没有对s1进行初始化赋值,因为名字和年龄变成私有变量后,不能通过等号的方式赋值,初始化的方式后面会讲)。下面重新定义合理的名字和年龄后,创建对象成功。

构造函数

  • 函数名与类名相同,且没有返回类型
  • 在创建对象时自动被调用,且仅被调用一次
  • ——对象定义语句
  • ——new操作符
  • 为成员变量赋初值,分配资源,设置对象的初始状态
  • 对象的创建过程
  • ——为整个对象分配内存空间
  • ——以构造实参调用构造函数->构造基类部分->构造成员变量->执行构造代码

下面通过构造函数来初始化对象:

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public:            // 公有成员访问控制符
    Student(const string& name,int age){   // 构造函数
        m_name = name;
        m_age = age;
    }
    void who(void) // 行为   成员函数
    {
        cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
    }

private:
    string m_name; // 成员变量
    int m_age;
};

// 把学生类变成对象(实例化)
int main(void)
{
    Student s1("张三",22);
    s1.who();
    Student s2("二货",23);
    s2.who();
}

类的声明和实现可以分开

class 类名{

        返回类型 函数名 (形参表);     // 在类的里面只给出函数的声明

};

返回类型 类名::函数名 (形参表){     // 在类的外面给出函数定义

        函数体;

}

 例如,我把构造函数和who函数的定义放到类外:

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public: // 公有成员访问控制符

    Student(const string& name,int age);  // 声明
    void who(void);

private:
    string m_name; // 成员变量
    int m_age;
};

// 在类的外面定义函数
Student::Student(const string& name,int age){
    m_name = name;
    m_age = age;
}

void Student::who(void){
    cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
}


// 把学生类变成对象(实例化)
int main(void)
{
    Student s1("张三", 22);
    s1.who();
    Student s2("二货", 23);
    s2.who();
    return 0;
}

 对象的创建与销毁

  • 在栈中创建单个对象

类名  对象;   // 注意不要加空括号

类名  对象(实参表);

  • 在栈中创建对象数组 

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

类名  对象数组[元素个数]  = {类名  (实参表),...};

类名  对象数组[ ] = {类名 (实参表),...};

销毁在缺省构造函数的下面 

 缺省构造函数

  • 有时必须自己定义缺省构造函数,即使它什么也不做,尤其是在使用数组或容器的时候,某些基于早期C++标准的编译器不支持对象数组的初始化语法
  • 有时必须为一个类提供缺省构造函数,仅仅因为它可能作为另一个类的子对象而被缺省构造
  • 若子对象不宜缺省构造,则需要为父对象提供缺省构造函数,并在其中显式地以非缺省方式构造该子对象

 下面为Student构造函数加入缺省参数

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public: // 公有成员访问控制符

    Student(const string& name = "新人",int age = 18);  // 声明   加入缺省参数
    void who(void);

private:
    string m_name; // 成员变量
    int m_age;
};

// 在类的外面定义函数
Student::Student(const string& name,int age){
    m_name = name;
    m_age = age;
}

void Student::who(void){
    cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
}


// 把学生类变成对象(实例化)
int main(void)
{
    Student s1("张三", 22);
    s1.who();
    Student s2("二货", 23);
    s2.who();
    Student s3;  // 创建一个学生对象不进行初始化,默认取缺省参数的值
    s3.who();
    return 0;
}

 下面创建一个对象数组

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public:                                                 // 公有成员访问控制符
    Student(const string &name = "新人", int age = 18); // 声明
    void who(void);

    void learn(const string &course) // 用引用是为了防止复制的开销,加const是为了防止意外的修改
    {
        cout << "我在学习" << course << "课程。" << endl;
    }
    void setName(const string &name)
    {
        if (name == "二货")
        {
            cout << "你才是二货!!!" << endl;
            return;
        }
        m_name = name;
    }
    void setAge(int age)
    {
        if (age <= 0)
        {
            cout << "非法年龄!" << endl;
            return;
        }
        m_age = age;
    }

private:
    string m_name; // 成员变量
    int m_age;
};

// 在类的外面定义函数
Student::Student(const string &name, int age)
{
    m_name = name;
    m_age = age;
}

void Student::who(void)
{
    cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
}

// 把学生类变成对象(实例化)
int main(void)
{
    // 采用无参方式构造数组
    Student sa[3];
    for (size_t i = 0; i < 3; i++)
    {
        sa[i].who();
    }
    // 采用有参方式构造数组
    Student sc[3] = {    // 这里括号里的3也可以不写
        Student("张飞", 22), Student("马超", 23), Student("鲁班", 2)};
    for (size_t i = 0; i < 3; i++)
    {
        sc[i].who();
    }

    return 0;
}

  • 在堆中创建/销毁单个对象

类名*对象指针 = new 类名;

类名*对象指针 = new 类名();

类名*对象指针 = new 类名(实参表);

delete 对象指针

  • 在堆中创建/销毁对象数组 

类名* 对象指针数组 = new 类名[元素个数];

类名* 对象指针数组 = new 类名[元素个数]{ 类名 (实参表),....};

// 上面的写法需要编译器支持C++11标准,关于C++标准,请参考《C++语言标准》

delete[ ] 对象数组指针;

具体示例如下:

#include <iostream>
using namespace std;
class Student // 创建学生类
{
public: // 公有成员访问控制符
    Student(const string &name = "新人", int age = 18); // 声明
    void who(void);

private:
    string m_name; // 成员变量
    int m_age;
};

// 在类的外面定义函数
Student::Student(const string &name, int age)
{
    m_name = name;
    m_age = age;
}

void Student::who(void)
{
    cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
}

// 把学生类变成对象(实例化)
int main(void)
{
    // 在堆中创建单个对象
    Student* ps = new Student;
    ps->who();
    delete ps;
    ps = new Student(); // 这个括号可加可不加,看个人习惯
    ps->who();
    delete ps;
    ps = new Student("张飞",22);
    ps->who();
    delete ps;
    cout << "-------------------------------" << endl;

    // 在堆中创建对象数组,无参数
    Student* pw = new Student[3];
    for (size_t i = 0; i < 3; i++)
    {
        pw[i].who();
    }
    // 销毁数组
    delete[] pw;
    cout << "-------------------------------" << endl;
    
    // 有参方式创建数组
    Student* pe = new Student[3]{
        Student("鲁班",2),Student("孙尚香",19),Student("刘备",20)
    };
    for (size_t i = 0; i < 3; i++)
    {
        pe[i].who();
    }
    delete[] pe;

    return 0;
}

 将类的声明、实现与使用分别放在不同文件里

在实际工作中都是分开的,如下图:

 将上述Student分开写为:先看一下整体的目录:

 头文件student.h的内容,存放的是Student类的声明:

// Student类的声明
#ifndef _STUDENT_H
#define _STUDENT_H
#include <iostream>
using namespace std;
class Student{
public:
    Student(const string& name = "新成员",int age = 20);
    void who(void);
    void learn(const string& course);
    void setName(const string& name);
    void setAge(int age);
    
private:
    string m_name;
    int m_age;
};
#endif  // _STUDENT_H

再看student.cpp的内容,存放的是Student类的实现:

// Student类的实现
#include "student.h"
Student::Student(const string& name, int age)
{
    m_name = name;
    m_age = age;
}
void Student::who(void)
{
    cout << "我是" << m_name << ",今年" << m_age << "岁." << endl;
}

void Student::learn(const string& course)
{
    cout << "我在学习" << course << "课程。" << endl;
}

void Student::setName(const string &name)
{
    if (name == "二货")
    {
        cout << "你才是二货!!!" << endl;
        return;
    }
    m_name = name;
}

void Student::setAge(int age)
{
    if (age <= 0)
    {
        cout << "非法年龄!" << endl;
        return;
    }
    m_age = age;
}

然后是main.cpp,使用Student类: 

//使用Student类

#include "student.h"
int main(void){
    Student luban("鲁班",2);
    luban.who();
    luban.learn("二级能接闪现");
    return 0;
}

编译并输出如下:

值得注意的是,在编译的时候我在后面加上了student.cpp,正常来说直接编译main.cpp就好了,但是我遇到了一个错误:

 如果你也遇到了和我一样的问题,可以参考这篇文章:《"undefined reference to XXX"问题总结》https://zhuanlan.zhihu.com/p/81681440

练习:编写一个动态时钟,在终端显示当前时间,有兴趣可以看一下-->链接

构造函数可以重载

  • 构造函数也可以通过参数表差别化形成重载
  • 重载的构造函数通过构造实参的类型选择匹配
  • 不同的构造函数版本表示不同的对象创建方式
  • 使用缺省参数可以减少构造函数重载版本数量
  • 某些构造函数具有特殊的意义:
  • ——缺省构造函数:按缺省方式构造
  • ——类型转换构造函数:从不同类型的对象构造
  • ——拷贝构造函数:从相同类型的对象构造

来看一个重载的例子:

#include <iostream>
using namespace std;
class Student
{
public:
    // 下面的四个构造函数属于重载关系
    Student(void)
    {
        m_name = "新人";
        m_age = 20;
    }
    Student(const string &name)
    {
        m_name = name;
        m_age = 20;
    }
    Student(int age)
    {
        m_name = "新人";
        m_age = age;
    }
    Student(const string &name, int age) // 这个函数可以代替这四个函数
    {
        m_name = name;
        m_age = age;
    }
    void show(void)
    {
        cout << m_name << "," << m_age << endl;
    }

private:
    string m_name;
    int m_age;
};

int main()
{
    Student s1;
    s1.show();
    Student s2("张飞");
    s2.show();
    Student s3(30);
    s3.show();
    Student s4("赵云", 12);
    s4.show();
}

 那么为什么说使用缺省参数可以减少构造函数重载版本的数量呢?其实上面的例子中,四个重载版本只需要最后一个版本即可,因为里面有两个缺省参数,覆盖了前面的三种情况。

缺省构造函数

  • 缺省构造函数也称无参构造函数,但其未必真的没有任何参数,为一个有参构造函数的每个参数都提供一个缺省值,同样可以达到无参构造函数的效果
  • 如果一个类没有定义任何构造函数,那么编译器会为其提供一个缺省构造函数
    • 对基本类型的成员变量(int,char这种),不做初始化
    • 对类类型(比如我们定义的Student类)的成员变量和基类子对象,调用相应类型的缺省构造函数初始化
  • 对于已经定义至少一个构造函数的类,无论其构造函数是否带有参数,编译器都不会为其提供缺省构造函数
  • 有时必须自己定义缺省构造函数,即使它什么也不做,尤其是在使用数组容器的时候,某些基于早期C++标准的编译器不支持对象数组的初始化语法
  • 有时必须为一个类提供缺省构造函数,仅仅因为他可能作为一另一个类的子对象而被缺省构造
  • 若子对象不宜缺省构造,则需要为父对象提供缺省构造函数,并在其中显式地以非缺省方式构造该子对象

来看一个例子:

#include <iostream>
using namespace std;

class A
{
public:
    A(int a) {}
};
class B // 没有构造函数,系统自己定义
{
public:
    B(void) : m_a(10) {} // 初始化表,显式,非缺省构造方式构造m_a
private:
    A m_a; // B里面有A类型的成员变量
};

int main()
{
    B b;   // A类的A是B类的子对象
    return 0;
}

 类型转换构造函数

  • 在目标类型中,可以接受单个源类型对象实参的构造函数,支持从源类型到目标类型的隐式类型转换

class 目标类型{

        目标类型(const 源类型& src){....}

};

  • 通过explicit关键字,可以强制这种通过构造函数实现的类型转换必须显式地进行

class 目标类型{

        explicit 目标类型(const 源类型& src){....}

};

来看一个例子:将小狗变成小猫:

#include <iostream>
using namespace std;

class Dog;  // 声明
class Cat
{
public:
    Cat(const string &name)
    {
        m_name = name;
    }
    // 类型转换操作函数  Cat是转换目标  Dog是转换源
    explicit Cat(const Dog& dog);  // 声明  explicit是显式的,不加表示隐式
    void talk(void)
    {
        cout << m_name << ": 喵~~~喵~~~" << endl;
    }

private:
    string m_name;
};

class Dog
{
public:
    Dog(const string &name)
    {
        m_name = name;
    }
    void talk(void)
    {
        cout << m_name << ": 汪汪!!" << endl;
    }

private:
    string m_name;
    friend class Cat;  // 友元,Cat可以访问Dog的私有成员
};

Cat::Cat(const Dog& dog){   // 类外定义  dog可以转换为cat
    m_name = dog.m_name;
};

int main(void)
{
    Dog dog("小白");
    dog.talk();
    // 用static_cast显式地完成
    // Cat cat = dog;  隐式
    Cat cat = static_cast<Cat>(dog);  // 不同源  !!!!!!!!!!!!!11
    cat.talk();
    Cat cat2("咪咪");
    cat2.talk();
    //cat2 = dog;  隐式
    cat2 = static_cast<Cat>(dog);   // !!!!!!!!!!!!1
    cat2.talk();
    return 0;
}

输出如下:实际中常用的为显式类型转换,即需要加explicit关键字,隐式类型转换的语句已在代码中注明 

 ​​​​​​​

 拷贝构造函数

  • 用于从一个已定义的对象构造其同类型的副本,即对象克隆,格式如下: 

class 类名{

        类名(const 类名& that){ ... }

};

  •  如果一个类没有定义拷贝构造函数,那么编译器会为其提供一个缺省拷贝构造函数
    • 对基本类型成员变量,按字节复制
    • 对类类型成员变量和基类子对象,调用相应类型的拷贝构造函数
    • 所有系统定义的构造函数,其访问属性都是公有public
  • 如果自己定义了拷贝函数,编译器不再提供缺省拷贝构造函数,这时所有与成员复制有关的操作,都必须在自定义拷贝构造函数中编写代码完成
  • 若缺省拷贝构造函数不能满足要求,则需自己定义
  • 拷贝构造的时机(重要!一定要知道)
    • 用已定义对象作为同类型对象的构造实参
    • 对象的形式向函数传递参数
    • 从函数中返回对象
    • 某些拷贝构造过程会因编译优化而被省略
自定义构造函数系统定义构造函数

缺省构造函数

缺省拷贝构造函数

除拷贝构造函数意外的任何构造函数缺省拷贝构造函数
拷贝构造函数

下面看一个拷贝构造的例子:

#include <iostream>
using namespace std;
class Student
{
public:
    Student(const string &name, int age)
    {
        m_name = name;
        m_age = age;
    }
    // 拷贝构造函数
    Student(const Student& that){
        m_name = that.m_name;
        m_age = that.m_age;
    }
    void show(void){
        cout << m_name << " " << m_age << endl;
    }

private:
    string m_name;
    int m_age;
};

void foo(Student s){
    s.show();
}

int main(void)
{
    Student s1("张飞", 22);
    s1.show();
    Student s2(s1);  // s2就是s1的克隆
    s2.show();
    // 以对象的形式向函数传参,引发拷贝构造
    foo(s2);  // 这也是拷贝手法,即foo的参数s是s2的拷贝
    return 0;
}

析构函数

主要作用在于对象销毁前系统自动调用,执行一些清理操作

  • 析构函数,没有返回值也不写void
  • 函数名与类名相同,前面加~
  • 构造函数可以有参数,因此可以发生重载
  • 程序在调用对象的时候会自动调用构造,无需手动调用,而且只会调用一次
  • 其功能并不局限在释放资源上,它可以执行任何类的设计者希望在最后一次使用对象之后执行的动作
  • 通常情况下,若对象在其生命周期的最终时刻,并不持有任何动态分配的资源,可以不定义析构函数

格式: ~类名(){ }

 例子如下:

#include <iostream>
using namespace std;

class Student
{
public:
    Student(const string &name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void show(void)
    {
        cout << m_name << " " << m_age << endl;
    }

    // 析构函数,如果不提供,系统会提供一个空实现析构函数
    ~Student()
    {
        cout << "析构函数调用" << endl;
    }

private:
    string m_name;
    int m_age;
};

int main(void)
{
    Student s1{"张三", 22};
    s1.show();
    return 0;
}

深拷贝与浅拷贝

先来看一段代码:

#include <iostream>
using namespace std;

class Student
{
public:
    Student()
    {
        cout << "Student默认构造函数调用" << endl;
    }
    Student(int age, int height)
    {
        m_age = age;
        m_height = new int(height); // 用指针接收堆区的数据
        cout << "Student有参构造函数调用" << endl;
    }
    ~Student()
    {
        // 析构代码,将堆区开辟的数据做释放操作
        if (m_height != NULL)
        {
            delete m_height;
            m_height = NULL;
        }
        cout << "Student的析构函数调用" << endl;
    }

    int m_age;
    int *m_height; // 身高,指针类型,目的是开辟到堆里面去
};

void test01()
{
    Student s1(18, 160);
    cout << "s1的年龄为:" << s1.m_age << "身高为:" << *s1.m_height << endl;
    Student s2(s1); // 浅拷贝
    cout << "s2的年龄为:" << s2.m_age << "身高为:" << *s2.m_height << endl;
}

int main()
{
    test01();
    return 0;
}

 

 这就是浅拷贝带来的问题,其原理图如下所示:在执行析构函数的时候,s2释放了堆区的内容,s1再次释放就会报错。

这个时候就要利用深拷贝来解决,即自己定义一个拷贝构造函数,重新开辟一块内存,让s2指向新的堆区,数据一样,但是地址不一样,这样释放的时候就不会重复。

在Student类中加入深拷贝构造函数如下:

    // 拷贝构造函数
    Student(const Student &that)
    {
        m_age = that.m_age;
        // m_height = that.m_height;  // 编译器默认执行的是这条语句
        m_height = new int(*that.m_height);  // 重新开辟内存
    }

原理图如下所示:

 程序结束时应该调用析构函数将堆区释放。

  初始化列表

  • 通过在类的构造函数中使用初始化表,指明该类的成员变量如何被初始化
  • 数组和结构型成员变量需要用花括号”{}“初始化
  • 类的类类型成员变量基类子对象,必须在初始化表中显式初始化,否则将调动相应类型的缺省构造函数初始化
  • 类的常量型引用型成员变量,必须在初始化表中显式初始化
  • 类的成员变量按其在类中的声明顺序依次被初始化,而与其在初始化表中的顺序无关

构造函数():属性1(值1),属性2(值2).....{ } 

#include <iostream>
using namespace std;

//初始化列表
class Person
{
public:
    /*传统的初始化操作:
    Person(int a, int b, int c)
    {
        m_a = a;
        m_b = b;
        m_c = c;
    }
    */
   // 初始化列表:
    Person(int a,int b,int c):m_a(a),m_b(b),m_c(c){}
    int m_a, m_b, m_c;
};

void test01()
{
    Person p(10, 20, 30);
    cout << p.m_a << " " << p.m_b << " " << p.m_c << endl;
}

int main()
{
    test01();
    return 0;
}

this指针概念

 通过上一节我们知道在C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个类型的对象会共用一块代码,那么问题是:这一块代码是如何区分哪个对象调用自己的呢?

C++通过提供特殊的指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针指向的是被调用的成员函数所属的对象

this指针的用途

  • 当形参和成员变量同名时,可以用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可以用return *this

例如:看下面一段代码,先猜猜他会输出什么?

#include<iostream>
using namespace std;
class Person{
public:
    Person(int age){
        age = age;
    }
    int age;
};

void test01(){
    Person p1(10);
    cout << "p1的年龄为:"<< p1.age << endl;
}

int main(){
    test01();
    return 0;
}

这是因为系统认为黄色箭头指向的age是一个东西(当鼠标点击任意一个age时,可以看到这三个都有相同的背景色),而红色箭头指向的是另一个。

 

第一种解决方案就像我们前面代码里面的,将age写为m_age,代表这是一个成员变量。

第二种方法就是this指针:可以看到this指向的age和下面的int age变成了同一个变量。

 这个时候我们再次运行代码,输出10岁:

 上面的例子解释了this指针的第一个用途,解决名称冲突,下面的代码展示如何返回对象本身:

#include<iostream>
using namespace std;
class Person{
public:
    Person(int age){
        this->age = age;
    }
    // 定义一个函数,将一个person对象的age加到当前对象
    Person& PersonAddAge(Person &p){  // 如果想返回对象本体,需要用引用的方式返回
        this->age += p.age;
        return *this;
    }

    int age;
};

void test01(){
    Person p1(10);
    Person p2(10);
    // 由于该函数返回对象本体,所以可以继续调用函数,调用多少次都行
    p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1); //链式编程思想
    cout << "p2的年龄为:"<< p2.age << endl;
}

int main(){
    test01();
    return 0;
}

这种无限往后追加的编程方式叫做链式编程思想

常函数与常对象

在类成员函数的形参表之后,函数体之前加上const关键字,该成员函数的this指针即具有常属性,这样的成员函数被称为常函数

class 类名{

        返回类型 函数名(形参表)const{

                函数体;

        }

};

在常函数内部无法修改成员变量的值,除非该成员变量被mutable关键字修饰。 

来看一个例子:定义一个电子文档,定义常函数print,使用户在print里面不可以修改电子文档的内容,并且定义一个计数器,打印三次之后就不可以打印。

#include <iostream>
using namespace std;
// 定义一个电子文档
class Document
{
public:
    Document(const string &content) : // 一种初始化的方式
                                      m_content(content){}

    void print(void) const
    {
        if (++m_counter > 3)
        {
            cout << "交钱!!!" << endl;
            return;
        }
        cout << m_content << endl;
        // m_content = ""; 这条语句是程序员意外加入的,会导致电子文档的内容清空,但是我们并不想出现这种情况,就可以将print变为常函数
        // 但是如果我想实现这样一个功能:当打印3次之后,就不可以打印电子文档内容,这个时候就需要定义一个计数器
        // 但是我想在常函数里面修改计数器的值,此时应该加mutable关键字
    }

    void pay(float money)
    { // 付钱之后又可以打印10遍
        if (money >= 10000)
        {
            m_counter = 0;
        }
    }

private:
    string m_content;
    mutable int m_counter = 0; // 可以在常函数中做修改
};

int main()
{
    Document doc("Hello world!");
    for (size_t i = 0; i < 5; i++)
    {
        doc.print();
    }

    return 0;
}

  

  •  被const关键字修饰的对象、对象指针或对象引用,统称为常对象

const User user(...);

const User* cptr = &user;

const User& cref = user;

  • 通过对象只能调用函数,通过非常对象既可以调用函数,也可以调用非常函数
  • 原型相同的成员函数,常版本和非常版本构成重载
    • 常对象只能选择常版本
    • 非常对象优先选择非常版本,如果没有非常版本,也能选择常版本(因为访问范围缩小,编译器认为是安全的)

例如,将我们上例中的main函数改为:

int main()
{
    Document doc("Hello world!");  // 非常对象
    for (size_t i = 0; i < 5; i++)
    {
        doc.print();  // 调用常函数
    }
    doc.pay(20000);  // 调用非常函数
    // 定义一个常对象
    const Document& cd = doc;
    cd.print();
    cd.pay(20000);

    return 0;
}

由于cd是一个常对象,而pay是非常函数,所以编译错误:

 再来看一个常函数与非常函数构成重载的例子:

#include <iostream>
using namespace std;

class A{
public:
    void foo(void){
        cout << "我是非常函数" << endl;
    }
    void foo(void) const{
        cout << "我是常函数" << endl;
    }
    void print(void) const{
        cout << "print只有我一个常版本的,你只能选择我喽" << endl;
    }
};

int main(){
    A a1;  // 非常对象
    a1.foo();
    const A a2;  // 常对象
    a2.foo();
    a1.print(); // 优先选择非常函数,如果没有,也可以选择常函数
    return 0;
}

静态成员

静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员

静态成员分为静态成员变量和静态成员函数

  • 静态成员属于类而不属于对象
  • 静态成员变量不包含在对象实例中,进程级生命周期
  • 静态成员函数没有this指针,也没有常属性
  • 静态成员依然受类作用与和访问控制限定符的约束 

静态成员变量:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化 

静态成员函数:

  • 所有对象共享一个函数
  • 静态成员函数只能访问静态成员变量 

先来看一个静态成员变量的例子:

#include <iostream>
using namespace std;

//静态成员变量
class Bank
{
public:
    // 所有对象共享同一份数据    类内声明,类外初始化
    static int money;
    void draw(int a);  // 取钱
    void show();  // 查看余额
private:
    static string b_name;  // 私有静态成员变量,类外无法访问
};

int Bank::money = 100;  // 余额的初始值

// 类外定义
void Bank::draw(int a)
{
    money -= a;
}
void Bank::show()
{
    cout << "余额为:" << money << endl;
}

int main()
{
    Bank b1, b2;
    b1.show();
    b1.draw(50);
    b1.show();
    b2.draw(20);
    b2.show();
    // 通过类名访问静态成员变量:
    cout << Bank::money << endl;
    return 0;
}

 

 再来看静态成员函数的例子:

#include <iostream>
using namespace std;
class Person
{
public:
    static void func()
    {
        m_age = 100;  // 静态函数可以访问静态变量
        // m_height = 180;  // 错误,不可以访问非静态成员
        cout << "static void func调用" << endl;
    }
    static int m_age;  // 静态成员变量
    int m_height;
private:
    static void func2(){   // 访问权限!
        cout << "static void func222调用" << endl;
    }
};

int Person::m_age = 0;

void test01()
{
    Person p;
    p.func();
}

int main()
{
    // 通过对象访问静态函数
    test01();
    //通过类访问静态函数(不需要创建对象)
    Person::func();
    return 0;
}

 成员变量和成员函数分开存储

  • C++给每个空对象分配一个字节空间,是为了区分空对象占内存的位置,每个空对象的内存地址独一无二
  • 非静态成员变量和静态成员变量分开存储
  • 成员变量和成员函数分开存储,非静态成员函数也不属于类的对象

单例模式(Singleton)

  • 一个类仅有一个实例,通过全局访问点获取
  • 将包括拷贝构造函数在内的所有构造函数私有化,防止类的使用者从类的外部创建对象
  • 公有静态成员函数getInstance()是获取对象实例的唯一渠道
  • 饿汉式:无论用不用,程序启动即创建
  • 懒汉式:用的时候创建,不用了即销毁
    • 永不销毁
    • 引用计数
    • 线程安全

下面来看饿汉模式的例子:

// 饿汉模式
#include <iostream>
using namespace std;
class Singleton{
public:
    static Singleton& getInstance(void){
        return s_instance;
    }
private:
    Singleton(void){}    
    Singleton(const Singleton& that){}
    static Singleton s_instance;
};

Singleton Singleton::s_instance;

int main(){
    Singleton& s1 = Singleton::getInstance();
    cout << &s1 << endl;
    Singleton& s2 = s1.getInstance();
    cout << &s2 << endl;
    return 0;
}

 

再来看懒汉模式的例子:

// 懒汉模式
#include <iostream>
using namespace std;
class Singleton{
public:
    static Singleton& getInstance(void){
        if(! s_instance){  // 第一次用的时候才new
            s_instance = new Singleton;
            ++s_counter;  // 每次调用就给计数器加一,表示有多少人拿到了这个实例
        }
        return *s_instance;
    }
    // 懒汉的一点改进:释放内存
    void releaseInstance(void){  // 不想用的时候就将内存释放
        if(s_counter && --s_counter == 0){  // 当所有人都不用这个实例的时候,释放内存
            delete this;
        }
    }
private:
    Singleton(void){
        cout << "构造" << endl;
    }    
    Singleton(const Singleton& that){}
    static Singleton* s_instance;
    static size_t s_counter;  // 定义一个计数器
    ~Singleton(void){  // 析构函数,改进
        cout << "析构函数" << endl;
        s_instance = NULL;
    }
};

Singleton* Singleton::s_instance = NULL;
size_t Singleton::s_counter = 0;

int main(){
    Singleton& s1 = Singleton::getInstance();
    cout << &s1 << endl;
    Singleton& s2 = s1.getInstance();
    cout << &s2 << endl;
    s2.releaseInstance();
    s1.releaseInstance();  // 当两个对象都释放的时候,才真正释放内存,运行析构函数
}

可以看到析构函数只调用了一次,因为只有当所有对象都释放后才会调用。 

成员指针

 成员变量指针

类型 类名::*成员变量指针;

成员变量指针=&类名::成员变量;

对象.*成员变量指针

对象->*成员变量指针

其本质就是特定成员变量在对象实例中的相对地址,解引用时再根据调用对象的地址计算出该成员变量的绝对地址

 来看一个例子:

#include <iostream>
using namespace std;
class Student
{
public:
    Student(const string& name,int age):m_name(name),m_age(age){}
    string m_name;
    int m_age;
};

int main(void)
{
    // 成员变量指针,指向的是Student中类型为string的成员变量,如果想让它指向m_name,可以这么写:
    string Student::*p1 = &Student::m_name;
    // 再定义一个指向m_age的成员变量指针
    int Student::*p2 = &Student::m_age;
    Student sa("张飞",23);
    // 解引用
    cout << sa.*p1 << endl;
    cout << sa.*p2 << endl;
    // 定义一个新的对象,让p1 p2指向新的对象,再次解引用
    Student sc("赵云",22);
    cout << sc.*p1 << endl;
    cout << sc.*p2 << endl;
    return 0;
}

  

 从例子中可以看到,成员指针只进行一次初始化,就可以指向多个对象(普通指针只能指向一个对象)

成员函数指针

返回类型(类名::*成员函数指针)(形参表);

成员函数指针 = &类名::成员函数名;

(对象.*成员函数指针)(实参表)

(对象指针->*成员函数指针)(实参表)

虽然成员函数并不存储在对象中,但也要通过对象或者对象指针对成员函数的指针解引用,其目的只有一个,即提供this指针 

#include <iostream>
using namespace std;
class Student
{
public:
    Student(const string& name,int age):m_name(name),m_age(age){}
    void who(void) const{
        cout << m_name << ", " << m_age << endl;
    }
    string m_name;
    int m_age;
};

int main(void)
{
    // 成员变量指针,指向的是Student中类型为string的成员变量,如果想让它指向m_name,可以这么写:
    string Student::*p1 = &Student::m_name;
    // 再定义一个指向m_age的成员变量指针
    int Student::*p2 = &Student::m_age;
    Student sa("张飞",23);
    // 解引用
    cout << sa.*p1 << endl;
    cout << sa.*p2 << endl;
    // 定义一个新的对象,让p1 p2指向新的对象,再次解引用
    Student sc("赵云",22);
    cout << sc.*p1 << endl;
    cout << sc.*p2 << endl;
    // 成员函数指针
    void (Student::*p3)(void) const = &Student::who;
    (sa.*p3)();
    return 0;
}

 友元

  • 可以通过friend关键字,把一个全局函数,另一个类的成员函数或者另一个整体,声明为某个类的友元
  • 友元拥有访问授权类任何非公有成员的特权
  • 友元声明可以出现在授权类的公有、私有或者保护等任何区域,且不受访问控制限定符的约束
  • 友元不是成员,其作用域并不隶属于授权类,也不拥有授权类类型的this指针
  • 操作符函数常被声明为其参数类型的友元

操作符重载

为什么要进行操作符重载呢?为了满足我们的需求,比如正常的加法是 1+1 = 2

那么如果我有两个复数 1+2i  和 2+3i  ,他们直接相加,怎么得出 3+5i 呢?这个时候就需要进行操作符重载,使两个复数相加时返回我们想要的结果。

操作符标记与操作符函数

  • 操作符标记

单目操作符: -、++ 、-- 、* 、->等

双目操作符:+ 、 - 、 += 、 -= 、 >> 、 << 、[ ]等

三目操作符:? :

  • 操作符函数

1.在特定条件下,编译器有能力把一个由操作数和操作符共同组成的表达式,解释为对一个全局或成员函数的调用,该全局或成员函数被称为操作符函数

2.通过定义操作符函数,可以实现针对自定义类型的运算法则,并使之与内置类型一样参与各种表达式

  •  双目操作符表达式:L#R

成员函数形式:L.operator#(R)

操作数是调用对象操作数是参数对象

全局函数形式:  ::operator#(L , R)

操作数是第一参数,操作数是第二参数

  • 单目操作符表达式:#O/O# 

成员函数形式: O.operator#( )

全局函数形式: ::operator#(O)

  • 三目操作符表达式 F#S#T  

无法重载 

 典型双目操作符的重载

  • 运算类双目操作符:+ - * / 等

左右操作符均可为左值或右值

表达式的值为右值

成员函数形式:

class LEFT{

        const RESULT operator#( const RIGHT& right) cosnt {...}

 };

全局函数形式

const RESULT operator#(const LEFT& left , const RIGHT& right){...}

下面来看一个例子:用成员函数重载+号,用全局函数重载-号,实现复数的加减

#include <iostream>
using namespace std;

class Complex
{
public:
    Complex(int r = 0, int i = 0) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << '+' << m_i << 'i' << endl;
    }
    // 下面第一个const 是为了返回右值
    // 第二个const是为了右操作数既能够接受左值,也能接受右值,即c2可以是 const Complex c2
    // 第三个const是为了左操作数可以是const属性,即c1可以是const类型的
    const Complex operator+(const Complex &r) const
    {
        return Complex(m_r + r.m_r, m_i + r.m_i);
    }

private:
    int m_r;
    int m_i;
    friend const Complex operator-(const Complex &l, const Complex r); // 友元函数
};

// 全局函数形式
const Complex operator-(const Complex &l, const Complex r)
{
    return Complex(l.m_r - r.m_r, l.m_i - r.m_i);
}

int main(void)
{
    Complex c1(1, 2);
    Complex c2(2, 3);
    Complex c3 = c1 + c2;
    c3.print();
    Complex c4 = c1 - c2;
    c4.print();
    return 0;
}

 ​​​​​​​​​​​​​​

  •  赋值类双目操作符:  =   -=   +=  *=   /= 等

右操作数为左值或者右值,但操作数必须是

表达式的值为左值,且为左操作数本身(而非副本) 

成员函数形式:

class LEFT{

        LEFT& operator#(const RIGHT& right){ ... }

};

全局函数形式:

LEFT& operator#(LEFT& left, const RIGHT& right){ ... }

 下面用成员函数形式重写 +=  ,用全局函数重写 -=  :

#include <iostream>
using namespace std;

class Complex
{
public:
    Complex(int r = 0, int i = 0) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << '+' << m_i << 'i' << endl;
    }

    Complex &operator+=(const Complex &r)
    {
        m_r += r.m_r;
        m_i += r.m_i;
        return *this;
    }

private:
    int m_r;
    int m_i;
    friend Complex& operator-=(Complex &l, const Complex& r); // 友元函数
};

// 全局函数形式
Complex& operator-=(Complex &l, const Complex& r)
{
    l.m_r -= r.m_r;
    l.m_i -= r.m_i;
    return l;
}

int main(void)
{
    Complex c1(1, 2);
    Complex c2(2, 3);
    const Complex c3(5, 6);
    c1 += c2; // 3+5i
    c1.print();
    (c1 += c2) += c3; // 10+14i
    c1.print();
    c1 -= c2;
    c1.print();  // 8+11i
    return 0;
}

 典型单目操作符的重载

  • 运算类单目操作符: - ~ ! 等

操作数为左值或右值

表达式的值为右值

成员函数形式:

class OPERAND{

        const RESULT operator# (void) const {...}

} ;  

其中 OPERAND是操作数的类型  # 是操作符

全局函数形式

const RESULT operator#(const OPERAND& operand){ ... }

来看一个例子:用成员函数形式重写 - 取反 ,用全局函数重写~,使其实部虚部互换

#include <iostream>
using namespace std;

class Complex
{
public:
    Complex(int r = 0, int i = 0) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << '+' << m_i << 'i' << endl;
    }
    // 取负操作   第一个const是为了返回右值,第二个const是为了让我们的操作数具有常属性
    const Complex operator-(void) const
    {
        return Complex(-m_r, -m_i);
    }

private:
    int m_r;
    int m_i;
    friend const Complex operator~(const Complex o); // 友元函数
};

// 全局函数形式  用~表示实部虚部互换
const Complex operator~(const Complex o)
{
    return Complex(o.m_i,o.m_r);
}

int main(void)
{
    Complex c1(1, 2);
    Complex c2(2, 3);
    const Complex c3(5, 6);
    c1 = -c1;
    c1.print();
    c2 = ~ c1;  // 实部虚部互换
    c2.print();
    return 0;
}

  •  前自增自减类型单目操作符:前++  前--

操作数为左值

表达式的值为左值,且为操作数本身(而非副本)

成员函数形式

class OPERAND{

        OPERAND& operator#(void){ ...}

};

全局函数形式

OPERAND& operator# (OPERAND& operand){ ... ]

 来看一个例子:成员函数重写++  全局函数重写 --

#include <iostream>
using namespace std;

class Complex
{
public:
    Complex(int r = 0, int i = 0) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << '+' << m_i << 'i' << endl;
    }
    Complex& operator++(void){
        ++m_r;
        ++m_i;
        return *this;
    }


private:
    int m_r;
    int m_i;
    friend Complex& operator--(Complex& o); // 友元函数
};

// 全局函数形式  用~表示实部虚部互换
Complex& operator--(Complex& o){
    --o.m_r;
    --o.m_i;
    return o;
}

int main(void)
{
    Complex c1(1, 2);
    Complex c2 = ++++c1;
    c1.print();
    c2.print();
    Complex c3 = --c1;
    c3.print();
    return 0;
}

  •  后自增自减类型嗯单目操作符: 后++   后--

操作数为左值

表达式的值为右值,且为自增自减以前的值

成员函数形式

class OPERAND{

        const OPERAND operator# (int){...}     // 哑元int是为了形成重载,区分前++  和 后++

};

全局函数形式

const OPERAND operator#(OPERAND& operand , int){ ... }

例子如下:

#include <iostream>
using namespace std;

class Complex
{
public:
    Complex(int r = 0, int i = 0) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << '+' << m_i << 'i' << endl;
    }
    Complex& operator++(void){
        ++m_r;
        ++m_i;
        return *this;
    }
    const Complex operator++ (int){
        Complex old = *this;
        ++*this;
        return old;  // 返回相加以前的值
    }// 哑元int是为了形成重载,区分前++  和 后++


private:
    int m_r;
    int m_i;
    friend Complex& operator--(Complex& o); // 友元函数   前--
    friend const Complex operator--(Complex& o ,int);  // 后--
};

// 全局函数形式  用~表示实部虚部互换
Complex& operator--(Complex& o){
    --o.m_r;
    --o.m_i;
    return o;
}
const Complex operator--(Complex& o ,int){
    Complex old = o;
    --o;
    return old;
}


int main(void)
{
    Complex c1(1, 2);
    Complex c2 = c1++;
    c1.print();
    c2.print();
    Complex c3 = c2--;
    c3.print();
    c2.print();
    return 0;
}

输入输出操作符的重载 

  • 输出操作符: <<

左操作数为左值形式的输出流(ostream)对象,右操作数为左值或右值

表达式的值为左值,且为左操作数本身(而非副本)

左操作数的类型为ostream,若以成员函数形式重载该操作符,就应将其定义为ostream类的成员,该类为标准库提供,无法添加新的成员,因此只能以全局函数形式重载该操作符

ostream& operator << (ostream& os, const RIGHT& right){ ... } 

输出 >> 重载:

istream& operator >> (istream& is, RIGHT& right){ ... } 

#include <iostream>
using namespace std;

class Complex
{
public:
    Complex(int r = 0, int i = 0) : m_r(r), m_i(i) {}

private:
    int m_r;
    int m_i;
    friend ostream& operator << (ostream& os, const Complex& c){
        return os << c.m_r << "+" << c.m_i << "i";
    }
    friend istream& operator >> (istream& is, Complex& c){
        return is >> c.m_r >> c.m_i;
    }
};


int main(void)
{
    Complex c1(1, 2);
    cout << c1 << endl;
    Complex c2,c3;
    cin >> c2 >> c3;
    cout << c2 << "," << c3 << endl;
    return 0;
}

​​​​​​​

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Goafan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值