01-C++基础与面向对象.md 35 KB


title: C++基础与面向对象 tags: [

嵌入式Linux,
Qt,
C++,
面向对象,
类与对象,
构造析构,
this指针,
继承,
重载,
多态,
虚函数,
封装,
抽象类,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第一章 在Ubuntu下编写C++、第二章 C++基础;例程 Embedded-Qt-Tutorial/C++/01_hello_world~11_abstract_class"

C++基础与面向对象

💡 关联知识:[[05-Qt应用开发/02-Qt入门与项目结构]](Qt 本身就是 C++ 库,QMainWindowQPushButton 都建立在本文的继承与多态之上)、[[05-Qt应用开发/03-信号与槽]](signals/slots/Q_OBJECT 是 Qt 对 C++ 语法的扩展);延伸阅读:[[Linux+C+C++技术体系梳理/1. C语言基础/12. 结构体]](类可以理解为结构体的升级版本)

Qt 是建立在 C++ 之上的。要写出能看懂、能修改的 Qt 程序,不需要成为 C++ 专家,但必须吃透 类与对象、构造析构、this、继承、重载、多态、封装、抽象类 这几块概念——Qt 的每一个类、每一次"重写父类函数"、每一个 ui->setupUi(this) 都用到了它们。

本篇先交代 C++ 与 C 的差异、g++ 编译流程和命名空间,再以一条"狗类"主线把面向对象的核心概念逐个拆开,每个概念都配有 完整可编译的源码。读完即可独立写 C++ 小项目,也能顺畅进入 Qt。


1. C++ 简介与与 C 的区别

1.1 C++ 是什么

C++(c plus plus)是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,同时支持:

  • 过程化编程(和 C 一样)
  • 面向对象编程(类、继承、多态)
  • 泛型编程(模板)

C++ 由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计,最初命名为"带类的 C",1983 年更名为 C++。C++ 是 C 的超集:任何合法的 C 程序都是合法的 C++ 程序。

标准方面:C++98 是第一个标准,此后陆续有 C++03/11/14/17 等版本(本教程 .pro 中的 CONFIG += c++11 即启用 C++11)。要记住版本原则:固定一个版本,不要随意升级

学习 C++ 要理解概念,而不是深究语言技术细节。带着本篇的概念去学 Qt,遇到 publicvirtual、重写、重载时就能对上号。

1.2 C++ 与 C 的主要区别

原书第二章开篇即说明:指针、数据类型、变量类型、判断、循环等知识在 C 与 C++ 中基本一样,不再重复讲;重点讲 C++ 的新特性和面向对象。差异可归纳如下:

对比项 C C++
布尔类型 旧标准无 bool,用 int 代替 原生 booltrue / false
变量定义位置 传统上必须集中在程序(块)开头 随用随定义
初始化方式 int x = 100; 还支持 int x(100); 直接初始化
输入输出 scanf / printf + 格式符 cin / cout + << >>,无需格式符
命名空间 namespace,避免命名冲突
字符串 char[] + <string.h> std::string 类型,方便好用
编程范式 过程化 过程化 + 面向对象 + 泛型
数据结构升级 struct 只有数据 class = 数据 + 成员函数 + 访问控制
编译方式 gcc test.c -o test g++ test.cpp -o test

一句话:C 是 C++ 的子集,C++ 在保留 C 能力的同时,把"函数 + 数据"组织成了"类"。

1.3 C++ 环境设置

要编译 C++,需要 C 编译器 gcc(可选)和 C++ 编译器 g++。新装的 Ubuntu 默认没有:

# 安装 gcc 与 g++
sudo apt-get install gcc g++

# 安装其他常用库
sudo apt-get install lsb-core lib32stdc++6

# 查看版本,确认安装成功
g++ -v
gcc -v

若编译 Qt 时提示 Cannot find -lGL,还需补装 OpenGL:

sudo apt-get install libglu1-mesa-dev

1.4 第一个 C++ 程序

在终端创建目录并编辑源文件:

mkdir C++                  # 创建一个 C++ 目录
cd C++                     # 进入该目录
mkdir 01_hello_world       # 创建 01_hello_world 目录
cd 01_hello_world          # 进入该目录
vi 01_hello_world.cpp      # 编辑 cpp 文件,拷贝下面的内容

完整源码:

#include <iostream>
using namespace std;

int main()
{
    cout << "Hello, World!" << endl;
    return 0;
}

逐行解释:

  • 第 1 行 #include <iostream>:包含标准输入输出流头文件。注意不能写成 iostream.h——带 .h 的是非标准的 C 风格库,不带 .h 的是标准输入输出流,必须配合命名空间使用。
  • 第 2 行 using namespace std;:告诉编译器使用标准命名空间 stdcincoutendl 都属于 std,不写这句就得写 std::cout
  • 第 3 行 int main():主函数,程序入口。
  • 第 5 行 cout << "Hello, World!" << endl;:向屏幕输出并换行。<<插入运算符(被重载过),endl 是换行语句。
  • 第 6 行 return 0;:终止 main(),并把状态码返回给调用进程。

编译与运行:

g++ 01_hello_world.cpp -o 01_hello_world   # -o 指定输出的可执行文件名
./01_hello_world                           # 输出 Hello, World! 并换行

补充:<< 可以连续使用(如 cout << "A\n" << "B" << endl;),也可以直接用 C 的 \n 换行——这说明 C++ 可以直接使用 C 语言语句

1.5 C++ 新特性(2.1)

原书只强调两个常用新特性:

  1. 布尔类型 bool:C++ 比 C 新增的数据类型。新标准 C 语言也已支持 bool,但旧标准 C 没有,编译器无法解释。
  2. 变量可随用随定义、可直接初始化

    int x = 100;   // 传统写法
    int x(100);    // C++ 直接初始化,等价于 x = 100
    

1.6 C++ 的输入输出方式(2.2)

C 语言用 scanf / printf,C++ 用 cin / cout 代替,输入输出流程不变,关键字和用法变了。

C  :输入设备 → 输入流 scanf  → 变量 → printf   → 输出流 → 输出设备
C++:输入设备 → 输入流 cin    → 变量 → cout     → 输出流 → 输出设备

效率上 scanf / printf 更高,但 cin / cout方便

cout 语法:

cout << x << endl;     // x 可为任意数据类型,甚至表达式
cout << x << y;        // endl 可省略,仅表示换行
  • 正确:cout << x << y;(变量之间用 <<
  • 错误:cout << x, y << endl;(变量之间不能用逗号)

cin 语法:

cin >> x;              // x 可为任意数据类型
cin >> x >> y;         // 连续输入两个不同变量

1.7 命名空间 namespace(2.3)

using namespace std; 我们已经用过。它的本质是给名字加前缀,避免冲突

  • C++ 标准库定义了 vector 容器,你自己也写了个 vector 类,名字就冲突了。于是标准库把名字都放进 std,你要用就写 std::vector
  • ::作用域运算符std::cin 读作"std 命名空间下的 cin"。
  • 老是写全名很繁琐,在没有冲突时就可以写一句 using namespace std; 偷懒。

自定义命名空间示例(02_namespace_example.cpp):

#include <iostream>
using namespace std;

namespace A
{
    int x = 1;
    void fun() {
        cout << "A namespace" << endl;
    }
}
using namespace A;

int main()
{
    fun();          // 声明 using namespace A 后可直接调用
    A::x = 3;       // 修改 A 命名空间下的 x
    cout << A::x << endl;
    A::fun();       // 显式指定 A 命名空间下的 fun
    return 0;
}

逐行解释:

  • 第 4~10 行:自定义命名空间 A,内部有变量 x(初值 1)和函数 fun()
  • 第 11 行:using namespace A; 声明使用命名空间 A。
  • 第 14 行:声明后才能直接写 fun();,否则必须写成 A::fun();
  • 第 15 行:把 A::x 重新赋值为 3。
  • 第 16 行:打印 A::x
  • 第 17 行:显式调用 A::fun()

初学 Qt 时较少看到命名空间,但 Qt 里同样可以自定义命名空间并正常使用。


2. C++ 面向对象

面向对象的三大特征是 继承、多态、封装。下面以"狗"为主线,把每个概念都配一份例程源码。

2.1 类和对象(2.2.1)

  • 类(class):C++ 的核心特性,通常称为"用户定义的类型"。类用于指定对象的形式,包含数据表示法处理数据的方法,二者合称类的成员
  • 对象(object):从类实例化出来的具体事物。

类比:把"狗"抽象成一个类(有名字、年龄、会跑会叫),"旺财"就是对象。抽象时只挑出我们关心的几种属性即可。

从类实例化对象有两种方式:

方式 语法 内存位置 释放方式
栈中实例化 Dog dog1; 离开作用域自动释放
堆中实例化 Dog *dog2 = new Dog(); 必须 delete dog2;

完整源码(03_class_dog_example.cpp):

#include <iostream>
#include <string>
using namespace std;

class Dog
{
public:
    string name;
    int age;

    void run() {
        cout << "小狗的名字是:" << name << "," << "年龄是" << age << endl;
    }
};

int main()
{
    Dog dog1;

    dog1.name = "旺财";
    dog1.age = 2;
    dog1.run();

    Dog *dog2 = new Dog();

    if (NULL == dog2) {
        return 0;
    }
    dog2->name = "富贵";
    dog2->age = 1;
    dog2->run();

    delete dog2;
    dog2 = NULL;
    return 0;
}

逐行解释:

  • 第 5 行:定义 Dog 类,类名尽量贴近它描述的事物,一看就懂。
  • 第 7 行:访问限定符 public(公有的)。此外还有 private(私有的)、protected(受保护的)。不写访问限定符默认是 private。简单理解:访问限定符就是给成员变量和成员函数设置访问权限。
  • 第 8~11 行:字符串变量 name、整型变量 age,以及方法 run(),在里面打印狗名和年龄。string 是 C++ 的数据类型,方便好用,使用频率很高。
  • 第 18 行:从栈中实例化对象 dog1
  • 第 20~22 行:给 dog1 的成员赋值,并调用 run()
  • 第 24 行:从堆中实例化对象,凡是用关键字 new 的都是从堆中实例化。
  • 第 26 行:堆对象需要开辟内存,指针指向那块内存;若 new 申请失败则指针为 NULL,程序直接退出,这体现了严谨性。
  • 第 29~31 行:给 dog2 成员赋值并调用 run()
  • 第 34、35 行:释放内存,并将 dog2 重新指向 NULL

类与结构体的关系:类可以说是结构体的升级版本——结构体只有数据,类额外提供成员函数和访问控制。

2.2 构造函数与析构函数(2.2.1.1)

定义类时,如果没有自己写构造函数和析构函数,编译器会自动生成一个什么也不做的构造函数和析构函数,所以我们平时注意不到。

构造函数:在对象实例化时被系统自动调用,仅且调用一次。特点:

  1. 构造函数必须与类名同名
  2. 可以重载
  3. 没有返回类型,即使 void 也不行。

析构函数:与构造函数相反,在对象结束其生命周期时系统自动执行。特点:

  1. 格式为 ~类名()
  2. 调用时释放内存(资源);
  3. ~类名() 不能加参数
  4. 没有返回值,即使是 void 也不行。

完整源码(04_structor_example.cpp):

#include <iostream>
#include <string>
using namespace std;

class Dog
{
public:
    Dog();
    ~Dog();
};

int main()
{
    Dog dog;
    cout << "构造与析构函数示例" << endl;
    return 0;
}

Dog::Dog()
{
    cout << "构造函数执行!" << endl;
}

Dog::~Dog()
{
    cout << "析构函数执行!" << endl;
}

逐行解释:

  • 第 5~10 行:定义狗类,并声明构造函数与析构函数。
  • 第 14 行:用 Dog 类实例化对象 dog
  • 第 15 行:打印"构造与析构函数示例"。
  • 第 19~22 行:类的函数可以在类里实现,也可以在类外实现,在类外实现时需要用 ::。这里把构造函数定义在类外,打印"构造函数执行!"。
  • 第 24~27 行:同样在类外定义析构函数,打印"析构函数执行!"。

执行顺序可以预测:对象实例化时先调用构造函数(Dog() 先执行)→ 执行 main() 中的 cout → 对象生命周期结束时调用析构函数。

2.3 this 指针(2.2.1.2)

一个类中不同对象调用自己的成员函数时,其实调用的是同一段函数代码。那么成员函数如何知道要访问哪个对象的数据成员?答案就是 this 指针

  • 每个对象都拥有一个 this 指针,this 记录对象的内存地址。
  • 在 C++ 中,this 指针是指向类自身数据的指针,简单说就是指向当前类的当前实例对象。

特点:

  1. this 只能在成员函数中使用,全局函数、静态函数都不能使用。实际上成员函数默认第一个参数为 T * const this,即 int func(int p) 在编译器看来原型是 int func(T * const this, int p)
  2. this 在成员函数开始前构造,在成员函数结束后清除。
  3. this 的存放位置因编译器而异:可能在栈、寄存器,甚至全局变量。

完整源码(05_this_pointer_example.cpp):

#include <iostream>
#include <string>
using namespace std;

class Dog
{
public:
    string name;
    void func();
};

int main()
{
    Dog dog;
    dog.func();
    return 0;
}

void Dog::func()
{
    this->name = "旺财";
    cout << "小狗的名字叫:" << this->name << endl;
}

逐行解释:

  • 第 21、22 行:在成员函数里使用 this 指针指向类成员 name,先赋值为"旺财",再打印。程序输出"小狗的名字叫:旺财"。

后面学 Qt 时会经常遇到 this,例如 ui->setupUi(this)new QPushButton(this)——这里的 this 就是"当前窗口对象自身",作为父对象传给子控件。

2.4 继承(2.2.2)

继承允许我们依据另一个类来定义一个类,从而达到重用代码、提高执行效率的效果。

  • 基类(base class):已有的类。
  • 派生类(derived class):新建的类。

Qt 里大量使用继承:当 Qt 的类不满足要求时,就继承它并重写相应函数。

派生类定义形式:

class derived-class : access-specifier base-class

access-specifierpublicprotectedprivate 之一;不使用则默认为 private。继承方式:

继承方式 基类 public 成员 基类 protected 成员 基类 private 成员
public 公有继承 成为派生类 public 成为派生类 protected 不可直接访问(可通过基类 public/protected 成员间接访问)
protected 保护继承 成为派生类 protected 成为派生类 protected 不可直接访问
private 私有继承 成为派生类 private 成为派生类 private 不可直接访问

完整源码(06_inherit_example.cpp,公有继承):

#include <iostream>
#include <string>
using namespace std;

/*动物类,抽象出下面两种属性,
 *颜色和体重,是每种动物都具有的属性
 */
class Animal
{
public:
    /* 颜色成员变量 */
    string color;
    /* 体重成员变量 */
    int weight;
};

/*让狗类继承这个动物类,并在狗类里写自己的属性。
 *狗类拥有自己的属性name,age,run()方法,同时也继承了
 *动物类的color 和weight 的属性
 */
class Dog : public Animal
{
public:
    string name;
    int age;
    void run();
};

int main()
{
    Dog dog;
    dog.name = "旺财";
    dog.age = 2;
    dog.color = "黑色";
    dog.weight = 120;
    cout << "狗的名字叫:" << dog.name << endl;
    cout << "狗的年龄是:" << dog.age << endl;
    cout << "狗的毛发颜色是:" << dog.color << endl;
    cout << "狗的体重是:" << dog.weight << endl;
    return 0;
}

逐行解释:

  • 第 21 行:Animal 作为基类,Dog 作为派生类,Dog 公有继承了 Animal
  • 第 31~39 行:dog 对象既使用了自身成员 nameage,又使用了从 Animal 继承来的 colorweight,证明继承生效。

2.5 重载(2.2.3)

C++ 允许在同一作用域中为某个函数和运算符指定多个定义,分别称为函数重载运算符重载

  • 重载声明:与之前已在该作用域声明过的函数/方法同名,但参数列表和实现不同
  • 重载决策:调用时编译器把实参类型与各定义中的形参类型比较,选出最合适的定义。

2.5.1 函数重载(2.2.3.1)

同一作用域内可声明几个功能类似的同名函数,但形参(个数、类型或顺序)必须不同。不能仅靠返回类型不同来重载函数。

完整源码(07_func_overloading.cpp):

#include <iostream>
#include <string>
using namespace std;

class Dog
{
public:
    string name;
    void getWeight(int weight) {
        cout << name << "的体重是:" << weight << "kG" << endl;
    }

    void getWeight(double weight) {
        cout << name << "的体重是:" << weight << "kG" << endl;
    }
};

int main()
{
    Dog dog;
    dog.name = "旺财";
    dog.getWeight(10);
    dog.getWeight(10.5);
    return 0;
}

逐行解释:

  • 第 9 行:getWeight(int weight),以 int 类型为参数。
  • 第 13 行:同名函数 getWeight(double weight),参数类型不同,构成函数重载。
  • 第 22、23 行:分别传入 intdouble,程序匹配不同的重载函数。

注意:如果用户传入的参数类型不在任何重载函数里(例如 string),编译器匹配不到相应重载函数,编译时就会报错。可以多写几种类型(stringcharfloat 等)覆盖更多情况。

2.5.2 运算符重载(2.2.3.2)

运算符重载的实质就是函数重载或多态,目的是让人能用同名的函数完成不同的基本操作。需要使用特殊函数形式:

<返回类型说明符> operator <运算符符号>(<参数表>)
{
    <函数体>
}

可重载的运算符:

类别 运算符
双目算术 + - * / %
关系 == != < > <= >=
逻辑 `
单目 + - *(指针) &(取地址)
自增自减 ++ --
\| & ~ ^ << >>
赋值 = += -= *= /= %= &= \|= ^= <<= >>=
空间申请与释放 new delete new[] delete[]
其他 ()(函数调用) ->(成员访问) , []

不可重载的运算符:

运算符 含义
. 成员访问运算符
.* ->* 成员指针访问运算符
:: 域运算符
sizeof 长度运算符
?: 条件运算符
# 预处理符号

以重载 + 为例(08_operator_example.cpp),把两个 Dog 对象的体重相加,返回新的 Dog

#include <iostream>
#include <string>
using namespace std;

class Dog
{
public:
    int weight;
    Dog operator+(const Dog &d) {
        Dog dog;
        dog.weight = this->weight + d.weight;
        return dog;
    }

};

int main()
{
    Dog dog1;
    Dog dog2;
    Dog dog3;

    dog1.weight = 10;
    dog2.weight = 20;
    dog3 = dog1 + dog2;
    cout << "第三只狗的体重是:" << dog3.weight << endl;
    return 0;
}

逐行解释:

  • 第 9~13 行:重载 + 运算符。成员函数形式下,左操作数是 this,右操作数是参数 d,使用 this->weight 访问左对象成员,返回一个新的 dog 对象。
  • 第 25 行:dog1 + dog2 触发重载后的 +,普通算术运算符 + 本来不能把两个对象相加,这正是重载的意义。

实际开发中运算符重载用得较少,理解这种思想即可;但 Qt 里 <<cout 插入)本身就是重载的典型例子。

2.6 多态(2.2.4)

C++ 多态意味着:调用成员函数时,会根据调用函数的对象的类型执行不同的函数。

形成多态必须具备三个条件:

  1. 必须存在继承关系
  2. 继承关系中必须有同名虚函数(虚函数是在基类中用关键字 virtual 声明的函数;在派生类中重新定义基类的虚函数时,会告诉编译器不要静态链接到该函数);
  3. 存在基类类型的指针或引用,通过该指针或引用调用虚函数。

两个关键概念:

  • 虚函数:基类中用 virtual 声明的函数。声明形式 virtual ReturnType FunctionName(Parameter)虚函数必须实现,不实现编译器会报错。它让编译器不要静态链接,而改为动态链接(后期绑定)
  • 纯虚函数:基类中不能给出有意义实现时使用,声明形式 virtual void function1() = 0;。纯虚函数一定没有定义,用来规范派生类的行为(即接口)。包含纯虚函数的类是抽象类,抽象类不能实例化,但可以声明指向实现该抽象类的具体类的指针或引用。

完整源码(09_polymorphism_example.cpp):

#include <iostream>
#include <string>
using namespace std;

/* 定义一个动物类 */
class Animal
{
public:
    virtual void run() {
        cout << "Animal 的run()方法" << endl;
    }
};

/* 定义一个狗类,并继承动物类 */
class Dog : public Animal
{
public:
    void run() {
        cout << "Dog 的run()方法" << endl;
    }

};

/* 定义一个猫类,并继承动物类 */
class Cat : public Animal
{
public:
    void run() {
        cout << "Cat 的run()方法" << endl;
    }

};

int main()
{
    /* 声明一个Animal 的指针对象,注:并没有实例化 */
    Animal *animal;
    /* 实例化dog 对象 */
    Dog dog;
    /* 实例化cat 对象 */
    Cat cat;

    /* 存储dog 对象的地址 */
    animal = &dog;
    /* 调用run()方法 */
    animal->run();

    /* 存储cat 对象的地址 */
    animal = &cat;
    /* 调用run()方法 */
    animal->run();
    return 0;
}

逐行解释:

  • 第 9、18、28 行:三个类各有一个 run()。基类 Animalrun() 前面加了 virtual,成为虚函数。
  • 基类 Animal 声明指针 animal,通过基类指针访问 DogCat 对象的 run()前提是基类的 run() 必须声明为虚函数;若不声明,基类指针将访问到基类自己的 run()
  • 第 44、49 行:把派生类对象地址赋给基类指针。当基类 run() 是虚函数时,编译器不静态链接到该函数,而是链接到派生类的 run(),实现运行时多态。

可以尝试去掉 virtual 再编译:输出会变成两次 Animal 的run()方法,多态失效。

虚函数表的机制(扩展):

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。

编译器为含虚函数的类生成一张虚函数表(vtable),对象里保存一个指向该表的指针(vptr)。运行时通过 vptr 找到实际类型对应的函数地址,从而实现动态绑定:

flowchart LR
    animal["Animal* animal"] -->|指向| dogObj["Dog 对象"]
    dogObj -->|vptr| vtable["Dog 的 vtable\nrun() -> Dog::run"]
    animal2["Animal* animal"] -->|指向| catObj["Cat 对象"]
    catObj -->|vptr| vtable2["Cat 的 vtable\nrun() -> Cat::run"]

这也解释了为什么基类析构函数通常要声明为 virtual:通过基类指针 delete 派生类对象时,只有虚析构才能正确调用到派生类析构,避免资源泄漏。

2.7 数据封装(2.2.5)

封装是面向对象编程中把数据和操作数据的函数绑定在一起的概念,能避免外界干扰和误用,从而确保安全。数据封装引申出"数据隐藏"。

C++ 通过创建类来支持封装和数据隐藏,手段就是 publicprotectedprivate通常情况下把类成员状态设为 private,除非真的需要暴露,这样才能保证良好的封装性。

完整源码(10_encapsulation_example.cpp):addFood() 作为对外接口(public),total 作为私有成员(private),通过 getFood() 读取:

#include <iostream>
#include <string>
using namespace std;

class Dog
{
public:
    string name;

    Dog(int i = 0)
    {
        total = i;
    }

    void addFood(int number) {
        total = total + number;
    }

    int getFood() {
        return total;
    }
private:
    int total;
};


int main()
{
    Dog dog;

    dog.name = "旺财";

    dog.addFood(3);
    dog.addFood(2);

    cout << dog.name << "总共获得了" << dog.getFood() << "份食物" << endl;

    return 0;
}

逐行解释:

  • 第 10~13 行:在构造函数里初始化 total。不初始化的话 total 是随机的 int 值,所以一般都在构造函数里初始化;不要在类内直接赋值初始化,有可能某些编译器不支持。
  • 第 15~17 行:addFood(int number) 把获得的食物份数累加到 total
  • 第 19~21 行:getFood() 返回食物总份数,通过它访问私有成员 total
  • 第 8 行:string name 是 public 的成员,可以直接 dog.name = "旺财"
  • 第 33、34 行:添加食物份数。
  • 第 36 行:打印总份数。

2.8 数据抽象(2.2.6)

数据抽象是指只向外界提供关键信息,隐藏后台实现细节,即只表现必要的信息而不呈现细节。它是一种依赖接口和实现分离的编程(设计)技术,好处:

  1. 类的内部受到保护,不会因无意的用户级错误导致对象状态受损;
  2. 类实现可以随时间变化,以应对新需求或缺陷修复,而不改变用户级代码。

例子:手机能拍照、听音乐、收音,用户直接使用即可,不需要知道拍照是如何通过摄像头取像、如何在屏幕上显示的。C++ 类为数据抽象提供了可能:向外界提供大量用于操作对象数据的公共方法,外界并不清楚类的内部实现。

cout 本身就是一个公共接口,我们不必知道它如何在屏幕上显示内容,底层已经实现好了。

数据封装与数据抽象的区别:封装是把数据和操作数据的函数捆绑在一起的机制;抽象是仅向用户暴露接口而隐藏实现细节的机制。带公有和私有成员的类都可以作为数据抽象的实例(例子参考 2.7)。

2.9 接口(抽象类)(2.2.7)

接口描述类的行为和功能,而不需要完成类的特定实现。C++ 接口是用抽象类实现的:

  • 类中至少有一个函数被声明为纯虚函数,这个类就是抽象类。纯虚函数通过声明中的 = 0 指定。
  • 设计抽象类(通常称 ABC)的目的,是为其他类提供一个可继承的适当基类。
  • 抽象类不能实例化对象,只能作为接口使用;试图实例化抽象类对象会导致编译错误。
  • 子类若要被实例化,必须实现每个虚函数;没有重写纯虚函数就尝试实例化该类的对象,也会导致编译错误。
  • 可用于实例化对象的类称为具体类

完整源码(11_abstract_class.cpp):与多态例程类似,只是 Animalrun() 定义为纯虚函数,不用实现,由 DogCat 重写:

#include <iostream>

using namespace std;

/* 定义一个动物类 */
class Animal
{
public:
    virtual void run() = 0;
};

/* 定义一个狗类,并继承动物类 */
class Dog : public Animal
{
public:
    void run() {
        cout << "Dog 的run()方法" << endl;
    }

};

/* 定义一个猫类,并继承动物类 */
class Cat : public Animal
{
public:
    void run() {
        cout << "Cat 的run()方法" << endl;
    }

};

int main()
{
    /* 实例化dog 对象 */
    Dog dog;

    /* 实例化cat 对象 */
    Cat cat;

    /*  dog 调用run()方法 */
    dog.run();

    /* cat 调用run()方法 */
    cat.run();

    return 0;
}

结果与 2.6 的多态例程相同,但体现了两种不同的思想:多态例程是"基类给默认实现,派生类可选择重写",抽象类例程是"基类只给接口,派生类必须实现"。

多态与抽象类对比:

对比项 多态(虚函数有实现) 接口(纯虚函数)
基类函数 virtual void run() {...} virtual void run() = 0;
基类可实例化 可以 不可以(抽象类)
派生类是否必须重写 否,不重写就用基类实现 是,否则派生类也无法实例化
典型用途 提供可覆盖的默认行为 定义统一接口/规范

Qt 中大量使用这两者:继承 QMainWindow 时可以重写 paintEvent() 等虚函数;而 QAbstractItemModel 这类抽象基类必须实现其纯虚接口才能使用。


3. 编译与运行

3.1 通用流程

所有例程都是单文件 C++ 程序(无 .h、无 .pro),在 Ubuntu 终端即可完成:

# 1. 创建目录并进入
mkdir -p C++/03_class_dog_example && cd C++/03_class_dog_example

# 2. 编辑源文件(vi/vim 或任意编辑器)
vi 03_class_dog_example.cpp

# 3. 编译
g++ 03_class_dog_example.cpp -o 03_class_dog_example

# 4. 运行
./03_class_dog_example

3.2 例程与源码文件对照

11 个例程(Embedded-Qt-Tutorial/C++/)与本文 2.x 各小节一一对应:01_hello_world(Hello World)、02_namespace_example(命名空间)、03_class_dog_example(类与对象)、04_structor_example(构造/析构)、05_this_pointer_example(this 指针)、06_inherit_example(继承)、07_func_overloading(函数重载)、08_operator_example(运算符重载)、09_polymorphism_example(多态)、10_encapsulation_example(数据封装)、11_abstract_class(抽象类/接口)。

3.3 常见编译问题

现象 可能原因 排查手段
undefined reference to ... 函数只声明未定义 检查是否写了函数体;构造函数/析构函数是否在类外漏写 Dog::
error: 'cout' was not declared 缺少头文件或命名空间 #include <iostream>using namespace std;
error: 'string' was not declared 漏包含 <string> #include <string>
cannot be used as a function 变量名与函数名冲突或少了分号 检查类定义末尾的 };
多态未生效 基类函数漏写 virtual 给基类函数加 virtual
内存/段错误 堆对象未 delete 或野指针 newdelete,释放后置 NULL

4. 面试精选

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。

Q1:构造函数和析构函数各有什么特点?什么时候被调用?

:构造函数与类名同名、可重载、无返回类型;在对象实例化时由系统自动调用,且仅调用一次,通常用来初始化成员。析构函数格式为 ~类名()、不能带参数、无返回值;在对象生命周期结束时自动调用,用于释放内存/资源。若用户未定义,编译器会自动生成一个空实现。栈对象离开作用域时析构,堆对象 delete 时析构。

Q2:this 指针是什么?能在静态函数里用吗?

this 指向当前类的当前实例对象,记录对象内存地址,每个对象各有一个。成员函数通过它区分"操作的是哪个对象的数据"。this 只能在非静态成员函数中使用,全局函数和静态函数中都不能使用(静态函数不依赖具体对象)。它在成员函数开始时构造、结束时清除,存放位置由编译器决定(栈、寄存器或全局变量)。

Q3:函数重载和运算符重载分别是什么?重载决策如何进行?

:两者都是同一作用域内为同名函数/运算符提供多个定义。函数重载要求形参的个数、类型或顺序不同,不能仅靠返回类型不同来区分。运算符重载使用 operator 加运算符符号的特殊函数形式,把运算符作用于自定义类型时转成函数调用;.::sizeof?:# 等不可重载。调用时编译器用实参类型与各定义形参比较,选出最合适的定义,这一过程叫重载决策。

Q4:什么是多态?形成多态需要哪些条件?虚函数和纯虚函数有何区别?

:多态指调用成员函数时根据对象的实际类型执行不同的函数。三个条件:存在继承关系;派生类与基类有同名虚函数(基类用 virtual 修饰);通过基类指针或引用调用虚函数。虚函数在基类中必须实现,派生类可选择重写;纯虚函数用 = 0 声明、没有实现,含有纯虚函数的类是抽象类,不能实例化,派生类必须实现所有纯虚函数才能实例化。本质上虚函数通过虚函数表实现动态(后期)绑定。

Q5:封装、抽象、抽象类三者的联系与区别是什么?

:封装是把数据和操作数据的函数捆绑在一起,并用 public/protected/private 控制访问,通常数据成员设为 private、通过公共接口操作,从而保护内部状态;抽象是只暴露接口、隐藏实现细节,是一种接口与实现分离的设计技术。二者的实例都是"带公有和私有成员的类"。抽象类是实现接口的 C++ 手段:只要类中有至少一个纯虚函数它就是抽象类,不能实例化,只能作为基类/接口被继承,派生类实现其全部纯虚函数后才能实例化。


内容来源:《I.MX6U嵌入式Qt开发指南V1.2》第一章 在Ubuntu下编写C++、第二章 C++基础,以及正点原子 Qt 开发指南例程源码 Embedded-Qt-Tutorial/C++/01_hello_world ~ 11_abstract_class