本系列文章主要是介绍一些 NDK 开发所需的基础知识,目录如下:
NDK 基础(一)—— C 语言知识汇总
NDK 基础(二)—— C++ 语言基础与特性1
NDK 基础(三)—— C++ 语言基础与特性2
NDK 基础(四)—— C++ 高级特性1
NDK 基础(五)—— C++ 高级特性2
1、可变参数、this、static、friend
1.1 可变参数
可变参数的简单使用
C++ 中的可变参数(Variadic Parameters)是一种特性,允许函数接收可变数量的参数。这使得我们可以定义接受不同数量或不同类型参数的通用函数。可变参数的处理是通过使用特殊的语法和标准库中的相关功能来实现的。
/**
* 可变参数的使用
* @param count 可变参数的数量
* @param ... 可变参数
*/
void sample1(int count, ...) {
va_list args;
// 1.初始化 args,将 args 指向可变参数列表的第一个参数
va_start(args, count);
for (int i = 0; i < count; ++i) {
// 2.从 args 中获取下一个参数,并将 args 移动到下一个参数位置
int num = va_arg(args, int);
cout << num << " ";
}
// 3.结束 args 的使用
va_end(args);
cout << endl;
}
int main() {
// 10 35 89
sample1(3, 10, 35, 89);
}
使用大致分如下几个步骤:
- 导入 cstdarg 库,声明一个空的 va_list 变量 args
- 调用 va_start(args, count) 初始化 args,count 是可变参数的数量(但实际上 count 这个位置随便传一个变量都行,只不过为了配合下面的 for 循环,因此这里传了 count,当然从使用规范上讲也最好是传 count)
- 通过 va_arg(args, int) 取出参数,int 是要取出的参数的类型
- 使用完毕通过 va_end(args) 结束对可变参数的访问
可变参数模板
虽然可变参数函数提供了处理可变数量参数的灵活性,但在 C++ 中,更推荐使用可变参数模板(Variadic Templates)来实现类型安全的可变参数函数,因为可变参数模板在编译时提供了更好的类型检查和错误处理能力。
在 C++ 中,可变参数的基本概念是使用省略号(...
)来表示函数的可变参数部分。可变参数的处理通常涉及以下两个主要组件:
-
可变参数模板(Variadic Templates):C++11 引入了可变参数模板的概念,它允许我们定义模板函数或类,其中模板参数部分包含可变数量的参数。通过使用模板的递归展开机制,可以在编译时处理可变数量的参数。
例如,下面是一个简单的可变参数模板函数,用于打印任意数量的参数:
#include <iostream> void print() { std::cout << std::endl; } // 通过递归展开可变参数模板来打印每个参数 template<typename T, typename... Args> void print(T value, Args... args) { std::cout << value << " "; print(args...); } int main() { print(1, 2, 3, "Hello", 4.5); return 0; }
-
可变参数模板的参数包展开(Parameter Pack Expansion):为了处理可变参数模板中的参数包,我们可以使用展开语法,将参数包展开为独立的参数序列。展开语法使用省略号(
...
)结合其他语法元素,可以在函数调用、函数参数列表和其他上下文中进行展开。例如,我们可以使用展开语法来遍历可变参数模板中的参数包,并对每个参数执行操作:
#include <iostream> void processArgs() {} // 函数通过展开参数包递归地处理每个参数,打印每个参数的值 template<typename T, typename... Args> void processArgs(T value, Args... args) { // 处理当前参数 value std::cout << value << std::endl; // 递归处理剩余的参数 processArgs(args...); } int main() { processArgs(1, 2.5, "Hello", 'A'); return 0; }
可变参数的使用可以极大地增加函数的灵活性和通用性。它可以用于实现格式化输出、可变参数的转发、函数重载的替代等场景。通过结合使用可变参数模板和参数包展开,我们能够在编译时处理不同数量和类型的参数,而无需提前定义固定数量的重载函数。
1.2 static 关键字
static 关键字的用法:
-
局部静态变量:函数内部声明的静态变量,只被初始化一次,每次函数调用时不会重新创建,而是保留上一次调用的值:
void foo() { static int count = 0; count++; std::cout << "Count: " << count << std::endl; } int main() { foo(); // 输出:Count: 1 foo(); // 输出:Count: 2 foo(); // 输出:Count: 3 return 0; }
-
全局静态变量:类内部声明的全局静态变量是属于整个类的共享变量,与类的实例无关。静态成员变量只有一个副本,被所有类的实例共享:
class MyClass { public: static int count; }; int MyClass::count = 0; int main() { MyClass obj1; MyClass obj2; obj1.count = 10; std::cout << "obj1.count: " << obj1.count << std::endl; // 输出:obj1.count: 10 std::cout << "obj2.count: " << obj2.count << std::endl; // 输出:obj2.count: 10 obj2.count = 20; std::cout << "obj1.count: " << obj1.count << std::endl; // 输出:obj1.count: 20 std::cout << "obj2.count: " << obj2.count << std::endl; // 输出:obj2.count: 20 return 0; }
-
静态函数:在类内部声明的静态函数属于类本身,而不是类的实例。静态函数可以直接通过类名进行调用,无需创建类的对象。它们没有隐式的
this
指针,因此无法访问非静态成员(变量与函数):class MyClass { public: static void staticFunc() { std::cout << "This is a static function." << std::endl; } }; int main() { MyClass::staticFunc(); // 输出:This is a static function. return 0; }
关于静态成员使用上的一些细节:
- 可以直接通过类名调用静态成员,如 Dog::update(),Dog::id
- 静态函数或成员变量在头文件中声明时需要带上 static,但是在源文件定义时不用带 static
此外,静态成员还有一些需要注意的地方:
- 静态成员都属于类而不属于对象,其生命周期贯穿于整个程序运行期间
- 静态成员无法直接访问非静态成员,否则编译报错(因为静态成员的生命周期长,对象成员的生命周期短,可能会出现静态调用非静态成员时,非静态成员还没有被创建的情况)
- 静态成员变量需要在类外部进行初始化,以便为其分配内存空间(原因在下面单独解释),并且初始化只能使用常量表达式或编译时已知的值。如果需要进行复杂的初始化操作,可以使用静态成员函数来初始化静态成员变量
解释一下为什么静态成员变量不能在类的内部初始化:
- 类的定义只是提供了一种规范或者模板,它描述了静态成员变量的类型和属性,但没有为其分配内存空间。因此,如果在类内部对静态成员变量进行初始化,无法为其分配内存空间,也无法确保在程序的其他地方能够访问和使用该静态成员变量。
- 静态成员变量是在编译时初始化,那么就需要使用编译器知道的值,即常量表达式或其他编译时已知的值。在类的内部进行初始化时,无法保证初始化值是常量表达式或编译时已知的值,因为类的定义可能会随着程序的执行而发生变化。
那为什么非静态成员变量可以在类内部初始化?实际上在类定义时也没有为变量分配内存空间,是在运行时,程序为对象分配内存空间时,分配到某一个非静态成员变量,做初始化动作时根据定义的值为其初始化的,并不是在类内定义就立即为其分配空间并初始化了。
1.3 this 关键字
C++ 的 this 跟 Java 的 this 非常像,都是指向当前对象:
- 在 C++ 中 this 是一个指针常量,由编译器自动生成,其声明为 ClassName* const this,ClassName 是类名
- 作为指针常量的 this 指向固定地址,不可修改,但是其指向的对象的内容是可以修改的
- 当前对象,进一步解释就是调用成员变量或函数的那个对象,如 student.study(),那么 this 就是指向 student 对象的指针
如果在函数声明的最后加 const,表示该成员函数是一个常量成员函数,即在函数体内不会修改对象的成员变量。编译器会将 this
指针的类型自动调整为 const ClassName* const
,这意味着 this
指针本身是一个指向常量的指针,指向的对象也是常量对象,不可通过 this
指针修改对象的成员变量:
void changeAction() const {
// 地址不能改
// this = 0x43563;
// 地址对应的值不能改
// this->age = 100;
}
当然,this 的指针类型不是永久的变成 const ClassName* const,如果后续再调用非 const 函数,this 就又会变成原来的 ClassName* const。
1.4 友元函数与友元类
一般情况下私有成员外部无法访问,但是如果将一个函数声明为友元函数,就可以了:
class Person {
private: // 私有的 age,外界不能访问
int age = 0;
public:
Person(int age) {
this->age = age;
}
int getAge() {
return this->age;
}
// 定义友元函数 (声明,还没实现)
friend void updateAge(Person * person, int age);
};
// 友元函数的实现,可以访问、修改所有私有成员
void updateAge(Person* person, int age) {
person->age = age;
}
可以看到,友元函数在实现时,可以不用 friend 关键字,也不用在方法名之前加类的限定符 Person::,只需保证函数名、参数、返回值类型与声明一致即可。
如果在一个类内部声明了一个友元类对象,这个对象就可以访问该类所有的私有成员:
class ImageView {
private:
int viewSize;
friend class Class; // 友元类
};
// Java 每个类,都会有一个 Class,此 Class 可以操作 ImageView 私有成员
class Class {
public:
ImageView imageView;
void changeViewSize(int size) {
// 修改私有成员 viewSize
imageView.viewSize = size;
}
int getViewSize() {
// 访问私有成员 viewSize
return imageView.viewSize;
}
};
int main() {
Class mImageViewClass;
mImageViewClass.changeViewSize(600);
cout << mImageViewClass.getViewSize() << endl;
return 0;
}
2、运算符重载
不论是 C/C++/Java/Kotlin 默认都不支持类似两个对象直接相加的情况,比如对于这样一个类 Point:
class Point {
private:
int x, y;
...
}
试图通过两个 Point 对象相加得到一个新的 Point 编译会报错:
int main() {
Point point1(25, 106);
Point point2(34, 111);
// Invalid operands to binary expression ('Point' and 'Point')
Point point3 = point1 + point2;
}
这时我们可以通过运算符重载来实现这个功能。
2.1 + 操作符的重载
运算符重载一般有两种方式:
-
类外运算符重载:在类的外部进行运算符重载:
// 在类外进行操作符重载 Point operator+(Point point1, Point point2) { int x = point1.getX() + point2.getX(); int y = point1.getY() + point2.getY(); return {x, y}; }
-
类内运算符重载:在类的内部进行运算符重载:
Point operator+(Point point) { int newX = this->x + point.x; int newY = this->y + point.y; return {newX, newY}; }
以上两种方式还有一种加强形式,就是把参数声明为常量引用 const Point &point
,这样做的目的是:
- 提高性能,避免不必要的复制。这个在前面讲过,是对象引用的作用。当对象引用作为函数参数时,函数会直接使用该引用,而不会创建临时变量接收参数上的变量,减少了不必要的复制也就提高了性能
- 确保函数内部不会修改参数传递的对象。这是 const 的作用,编译器在编译时会进行检查,防止函数内部对 const 参数进行修改,从而增加代码的安全性和可维护性
当然,第二种形式的加强形式实现起来更方便一些,直接改参数即可:
Point operator+(const Point &point) {
int newX = this->x + point.x;
int newY = this->y + point.y;
return {newX, newY};
}
而第一种只改动参数还不够:
// 在类外进行操作符重载
Point operator+(const Point &point1, const Point &point2) {
// 'this' argument to member function 'getX' has type
// 'const Point', but function is not marked const
int x = point1.getX() + point2.getX();
int y = point1.getY() + point2.getY();
return {x, y};
}
编译报错,注释上给出了错误信息,意思是说,传递给成员函数 getX() 的 this 指针的是 const Point,说白了就是你让一个常量对象 Point 去调用 getX(),但是 getX() 并不是一个常量函数。由于 C++ 中常量对象只能调用常量函数,因此你需要将 getX() 修改为常量函数,getY() 同理:
// 方法名后加 const 使其变成常量成员函数
int getX() const;
int getY() const;
常量对象要求其状态保持不变,而非常量函数可能会修改对象的状态,因此常量对象不能调用非常量函数。也就是说,常量函数可以在常量对象上调用,而非常量函数只能在非常量对象上调用。
两种形式的使用方式都是 main() 中展现的那种形式,不过通过对比可以看出,类内运算符重载有几点优势:
- 类内可以直接访问私有成员,但类外只能通过对外暴露的方法访问私有成员
- 类内可以通过 this 获取当前对象的成员,这样重载函数只需要传一个参数就可以实现相同的效果
- 当重载函数参数使用常量引用时,类内重载的实现要更简单一些,修改的代码量较少
因此操作符重载大多是在类内部进行的。
2.2 自增操作符
还是以 Point 为例实现 ++ 的重载:
// ++Point
void operator++() {
this->x = this->x + 1;
this->y = this->y + 1;
}
// Point++
Point operator++(int) {
this->x = this->x + 1;
this->y = this->y + 1;
return *this;
}
空参的重载函数默认为是 ++Point,而 int 作为参数的认为是重载 Point++:
void sample2() {
Point point1(25, 106);
// 两种调用形式,对应两个重载函数不同的返回值,++point1 不能直接调用函数,因为其返回值为 void
++point1;
cout << point1.getX() << "," << point1.getY() << endl; // 26,107
cout << point1++.getX() << "," << point1.getY() << endl; // 27,108
}
2.3 输入输出操作符
我们从一个编译错误开始介绍输入输出操作符的重载。下面以输出操作符 <<
为例,输入操作符 >>
同理。
按照一个初学者的想法,输出操作符与 + 类似都是双目运算符,那么重载函数的写法也类似就可以了:
/**
* @param _START 输出流对象
* @param point 要输出的数据,为了不被意外更改,声明为常量引用
* @return 返回一个输出流的引用 ostream & 以进行链式调用
*/
ostream &operator<<(ostream &_START, const Point &point) {
_START << "Point(" << point.x << "," << point.y << ")" << endl;
return _START;
}
看似没什么问题,但是编译器报错了:Overloaded 'operator<<' must be a binary operator (has 3 parameters)
,意思是重载的操作符 <<
必须是一个双目运算符(有 3 个参数)。
看到这就有点懵了,双目运算符明明就两个参数,函数上也写的两个参数,怎么编译器报错就说需要三个参数了?这是因为 C++ 的成员函数都有一个隐式的参数 this,它指向当前对象的实例,允许在成员函数内部访问和操作对象的成员变量和其他成员函数。当你调用一个成员函数时,编译器会将调用该函数的对象的指针作为隐式参数传递给该函数。
好了,既然 this 是作为隐式参数成为第 3 个参数,我们再来观察两个显式的参数 ostream &_START 和 const Point &point。双目操作符重载函数的两个参数,实际上就是参与运算的两个数。这里我们能明显看出 <<
与 +
的不同,后者参与运算的两个对象是同类型的,比如都是 Point,而前者,运算符左侧对象是输出流 ostream,右侧是要输出的数据类型 Point。也就是说,以 ostream << Point
这种形式重载时,调用重载函数的是左侧的 ostream,传给函数的 this 就是 ostream。那么你就只能在 ostream 中将 <<
重载为成员函数,如果像例子这样,在 Point 内将其声明为成员函数,就会报上面的错,因为 Point 内的 this 是 Point 类型,而重载函数传进来的 this 一定是 ostream 类型,冲突了。
知晓以上原因后,解决办法就好想了,那就是在 Point 内将重载函数声明为友元函数:
/**
* << 的重载,因为两个操作数类型不同,因此不能在 Point
* 内将其声明为成员函数,只能声明为友元函数
* @param _START 输出流对象,ostream 定义在 std 空间中
* @param point 要输出的数据,为了不被意外更改,声明为常量引用
* @return 返回一个输出流的引用 ostream & 以进行链式调用
*/
friend ostream &operator<<(ostream &_START, const Point &point) {
_START << "Point(" << point.x << "," << point.y << ")" << endl;
return _START;
}
如果你是在 ostream 内重载,就可以把 <<
的重载函数声明为成员函数。当然,不把重载函数声明为任何一个类的成员函数,在类外重载也是可以的:
ostream &operator<<(std::ostream &START, const Point &point) {
START << "Point(" << point.getX() << "," << point.getY() << ")" << endl;
return START;
}
只不过因为无法直接访问 Point 的私有成员,需要通过其暴露的函数获取到 x 和 y。
实际上,所有两个操作数类型不同的双目操作符都会有类似的问题,就是不能在数据类中将操作符重载定义为成员函数。
2.4 下标操作符
我们定义一个 Array 类,在 Array 类内部重载 [] 操作符,使得 Array 可以使用数组的形式访问内部保存的数据。
首先在头文件中声明成员:
class Array {
private:
int size = 0;
int *head;
public:
Array();
void set(int, int);
int getSize();
int operator[](int);
};
上面将下标操作符重载为类内定义的成员函数,只需要一个下标作为显式参数。实现如下:
Array::Array() {
head = static_cast<int *>(malloc(sizeof(int)));
}
void Array::set(int index, int value) {
// 这个 [] 目前还不是重载的,或者也可以使用指针操作
// *(head + index) = value;
head[index] = value;
size++;
}
int Array::getSize() {
return size;
}
int Array::operator[](int index) {
return head[index];
}
void printArray(Array array) {
for (int i = 0; i < array.getSize(); ++i) {
// 可以通过数组的方式访问 Array 的数据
cout << array[i] << endl;
}
}
最后进行测试:
void sample4() {
Array array;
array.set(0, 1000);
array.set(1, 2000);
array.set(2, 3000);
array.set(3, 4000);
array.set(4, 5000);
printArray(array);
}
最后可以顺利的输出 Array 中保存的数据:
1000
2000
3000
4000
5000
3、继承
3.1 基本使用
class Person {
public:
char *name;
int age;
public:
Person(char *name, int age) : name(name) {
this->age = age;
cout << "Person 构造函数" << endl;
}
void print() {
cout << this->name << " , " << this->age << endl;
}
};
/**
* 1.如果只写 class Student : Person,那执行的就是默认的私有继承,
* 在 Person 前面有一个隐式的 private
* 2.私有继承:在子类里面可以访问父类的成员,但在子类外不可以
* 3.公有继承:在子类的内部和外部都可以访问父类的成员
*/
class Student : public Person {
private:
char *course;
public:
/**
* 1.子类构造函数可以通过冒号调用父类的构造函数对父类成员进行初始化
* 2.调用父类构造函数的同时,仍可以通过列表方式对子类的成员变量进行初始化
*/
Student(char *name, int age, char *course) : Person(name, age), course(course) {
cout << "Student 构造函数" << endl;
}
void test() {
cout << name << endl;
cout << age << endl;
print();
}
};
int main() {
Student stu("Tracy", 99, "C++");
// 只有公开继承,子类对象才可以在类外获取父类的成员
stu.name = "李四";
return 0;
}
有关公有继承与私有继承的解释请参考代码注释。下面要说一下继承关系中构造函数与析构函数的调用顺序问题:
- 假如 Student 继承了 Person,那么在通过 Student 构造函数创建 Student 对象时,会先调用 Person 的构造函数,然后才执行 Student 的构造函数
- 当 Student 对象的生命周期结束被回收时,先执行 Student 的析构函数,再执行 Person 的析构函数
3.2 多继承
Java 摒弃了 C++ 的多继承,而是采用单继承多实现,因为多继承可能会产生二义性,影响代码的健壮性。
先看一个简单的例子。假如三个父类中都定义了 onCreate() 和 show():
class BaseActivity1 {
public:
void onCreate() {
cout << "BaseActivity1 onCreate" << endl;
}
void show() {
cout << "BaseActivity1 show" << endl;
}
};
class BaseActivity2 {
public:
void onCreate() {
cout << "BaseActivity2 onCreate" << endl;
}
void show() {
cout << "BaseActivity2 show" << endl;
}
};
class BaseActivity3 {
public:
void onCreate() {
cout << "BaseActivity3 onCreate" << endl;
}
void show() {
cout << "BaseActivity3 show" << endl;
}
};
子类在继承这三个类的时候,重写了 onCreate() 而没有重写 show():
class MainActivity : public BaseActivity1, public BaseActivity2, public BaseActivity3 {
public:
void onCreate() {
cout << "MainActivity onCreate" << endl;
}
};
那么在子类对象调用函数时:
int main() {
// 1.子类对象调用子类函数,没有二义性
MainActivity mainActivity;
mainActivity.onCreate();
// 2.由于子类没有重写 show(),这里调用 show()
// 编译器就不知道该调用哪一个父类的函数,产生二义性
mainActivity.show();
return 0;
}
在调用 show() 时就会因为有二义性,使得编译器报错。解决方法有两种:
-
在调用 show() 时显式指定调用哪一个父类的 show():
mainActivity.BaseActivity1::show(); mainActivity.BaseActivity2::show(); mainActivity.BaseActivity3::show();
-
在子类中重写 show():
class MainActivity : public BaseActivity1, public BaseActivity2, public BaseActivity3 { public: void onCreate() { cout << "MainActivity onCreate" << endl; } // 解决方案二: 子类上重写父类的show() void show() { cout << "MainActivity show" << endl; } };
实际上,解决二义性还有第三种方案。假如有如下的继承关系:
// 祖父类
class Object {
public:
int number;
void show() {
cout << "Object show run..." << endl;
}
};
// 父类1
class BaseActivity1 : public Object {
};
// 父类2
class BaseActivity2 : public Object {
};
// 子类
class Son : public BaseActivity1, public BaseActivity2 {
};
子类 Son 在调用 show() 或者访问 number 时都会因为产生二义性报错。这时可以使用第三种解决方案,在两个父类继承祖父类的 public 前加上 virtual 关键字:
// 父类1
class BaseActivity1 : virtual public Object {
};
// 父类2
class BaseActivity2 : virtual public Object {
};
这样编译器就不会报错了,原因是这样就是把 Object 声明为“虚基类”,采用这种虚拟继承的 Son 中只有一份 Object 的实例,减少了冗余拷贝也避免了二义性。
需要注意:
- 虚基类的构造函数由最终派生类负责调用,并确保虚基类的构造函数只被调用一次,以保证只有一个实例
- 在继承中,使用
virtual
关键字进行虚继承并不会影响父类的成员变量和函数在子类中的拷贝 - 虚基类的作用是在多重继承中只继承一份该虚基类的实例,避免冗余拷贝和二义性的问题
4、多态
4.1 C++ 如何开启多态
与 Java 默认开启多态不同,C++ 的多态是默认关闭的。以如下代码为例:
class BaseActivity {
public:
void onStart() {
cout << "BaseActivity onStart" << endl;
}
};
class HomeActivity : public BaseActivity {
public:
void onStart() {
cout << "HomeActivity onStart" << endl;
}
};
class LoginActivity : public BaseActivity {
public:
void onStart() {
cout << "LoginActivity onStart" << endl;
}
};
void startToActivity(BaseActivity *baseActivity) {
baseActivity->onStart();
}
void deleteObject(BaseActivity *activity) {
if (activity) {
delete activity;
activity = NULL;
}
}
int main() {
// 1.编译时类型与运行时类型相同,都是某一个子类
HomeActivity *homeActivity = new HomeActivity();
LoginActivity *loginActivity = new LoginActivity();
startToActivity(homeActivity);
startToActivity(loginActivity);
deleteObject(homeActivity);
deleteObject(loginActivity);
cout << endl;
// 2.编译时类型是运行时类型的父类
BaseActivity *activity1 = new HomeActivity();
BaseActivity *activity2 = new LoginActivity();
startToActivity(activity1);
startToActivity(activity2);
deleteObject(activity1);
deleteObject(activity2);
cout << endl;
return 0;
}
由于没有开启多态,因此两种情况调用的都是父类中的函数:
BaseActivity onStart
BaseActivity onStart
BaseActivity onStart
BaseActivity onStart
开启多态的方法是在父类的函数前面加 virtual:
class BaseActivity {
public:
virtual void onStart() {
cout << "BaseActivity onStart" << endl;
}
};
再次运行示例代码的结果:
HomeActivity onStart
LoginActivity onStart
HomeActivity onStart
LoginActivity onStart
4.2 动态多态与静态多态
像上一节中通过虚函数 + 类继承的方式实现的多态也称为动态多态,与之对立的还有静态多态。
动态多态(Dynamic Polymorphism)和静态多态(Static Polymorphism)是多态性的两种不同形式:
-
静态多态(静态绑定):
静态多态是在编译时确定调用的函数或方法,也称为早期绑定(Early Binding)或静态绑定。它是通过函数或方法的重载实现的。重载允许在同一个作用域内使用相同的名称但具有不同的参数列表来定义多个函数或方法。编译器根据函数或方法的参数列表来选择合适的重载版本。静态多态性的解析发生在编译时,因此它具有较高的效率。静态多态示例:
void print(int num) { // 执行整数的打印逻辑 } void print(float num) { // 执行浮点数的打印逻辑 } int main() { int x = 10; float y = 3.14f; print(x); // 调用 print(int num) print(y); // 调用 print(float num) return 0; } ```
-
动态多态(动态绑定):
动态多态是在运行时根据对象的实际类型来确定要调用的函数或方法,也称为晚期绑定(Late Binding)或动态绑定。它是通过虚函数和继承关系实现的。虚函数是在基类中声明的,可以在派生类中进行重写。通过基类的指针或引用调用虚函数时,会根据指针或引用指向的对象的实际类型来选择调用相应的重写函数。动态多态性的解析发生在运行时,因此它具有更高的灵活性。动态多态性示例:
class Shape { public: virtual void draw() { // 执行基类的绘制逻辑 } }; class Circle : public Shape { public: void draw() override { // 执行圆形的绘制逻辑 } }; class Rectangle : public Shape { public: void draw() override { // 执行矩形的绘制逻辑 } }; int main() { Shape* shape1 = new Circle(); Shape* shape2 = new Rectangle(); shape1->draw(); // 根据 shape1 的实际类型调用 Circle 类的 draw() 函数 shape2->draw(); // 根据 shape2 的实际类型调用 Rectangle 类的 draw() 函数 delete shape1; delete shape2; return 0; } ```
总结:
- 静态多态(静态绑定)是在编译时确定调用的函数或方法,通过重载实现。
- 动态多态(动态绑定)是在运行时根据对象的实际类型确定调用的函数或方法,通过虚函数和继承关系实现。
- 静态多态性的解析发生在编译时,动态多态性的解析发生在运行时。
- 静态多态性具有较高的效率,动态多态性具有更高的灵活性。
4.3 纯虚函数
纯虚函数也是 C++ 中用于实现多态的重要概念。上一节我们讲到,在实现的函数前面加 virtual,该函数就成为一个虚函数。而对添加了 virtual 的函数不提供实现,而是在声明后添加 = 0,由子类实现的函数就是纯虚函数。
虚函数(Virtual Function)和纯虚函数(Pure Virtual Function)的对比:
-
虚函数:是在基类中声明且可以在派生类中被重写(覆盖)的函数。通过将基类的函数声明为虚函数,可以实现动态多态。派生类可以提供自己的实现来替代基类中的虚函数。在运行时,根据对象的实际类型,通过基类的指针或引用调用虚函数时,会调用相应的重写函数。
虚函数的声明方式为在函数声明前加上virtual
关键字,例如:class Base { public: virtual void foo() { // 基类中的虚函数实现 } }; class Derived : public Base { public: void foo() override { // 派生类中的重写函数实现 } }; ```
-
纯虚函数:是在基类中声明但没有提供实现的虚函数。它的声明方式为在函数声明后加上
= 0
,表示该函数是纯虚函数。纯虚函数在基类中只有声明,而没有实现,它的具体实现由派生类提供。
声明纯虚函数的目的是要求派生类必须提供自己的实现,而基类不能被实例化。一个包含纯虚函数的类称为抽象类,它只能作为基类被继承,不能直接创建对象。示例如下:class AbstractBase { public: virtual void foo() = 0; // 纯虚函数声明 }; class Derived : public AbstractBase { public: void foo() override { // 派生类中的纯虚函数实现 } }; ```
联系和区别:
- 虚函数和纯虚函数都用于实现多态性,允许在派生类中重写基类的函数。
- 虚函数是在基类中声明且可以提供默认的实现,它的重写是可选的。而纯虚函数是在基类中声明但没有提供实现,它的重写是强制的,派生类必须提供自己的实现。
- 包含纯虚函数的类被称为抽象类,不能直接实例化,只能作为基类被继承。
- 如果一个类中包含纯虚函数,那么它就是一个抽象类,派生类必须提供纯虚函数的实现才能被实例化,否则派生类也会被视为抽象类。
- 虚函数和纯虚函数都使用动态绑定,通过基类的指针或引用调用派生类(即实际的运行时类型)的函数,实现动态多态性
- 虚函数可以有默认实现,因此可以在基类中直接调用虚函数。而纯虚函数没有实现,只能在派生类中提供具体的实现。
以 Android 中常用的 Activity 初始化过程为例:
class BaseActivity {
private:
void setContentView(int layoutId) {
cout << "setContentView(" << layoutId << ")" << endl;
}
public:
void onCreate() {
setContentView(getLayoutId());
initView();
initData();
initListener();
}
// 所有纯虚函数,交给子类实现,如果子类没有实现所有的
// 纯虚函数,那子类就是一个抽象类
virtual int getLayoutId() = 0;
virtual void initView() = 0;
virtual void initData() = 0;
virtual void initListener() = 0;
};
class MainActivity : public BaseActivity {
int getLayoutId() {
// 假设 R.layout.activity_main 的值为 0x0001
return 0x0001;
}
void initView() {
cout << "initView()" << endl;
}
void initData() {
cout << "initData()" << endl;
}
void initListener() {
cout << "initListener()" << endl;
}
};
int main() {
// 抽象类不能实例化
// Variable type 'BaseActivity' is an abstract class
// BaseActivity baseActivity;
// 实现了抽象父类所有纯虚函数的子类不是抽象类,可以实例化
MainActivity mainActivity;
mainActivity.onCreate();
}
纯虚函数可以类比为 Java 中的抽象函数,一个类只要包含一个纯虚函数(C++)/ 抽象函数(Java),那么这个类就是抽象类,只不过 C++ 没有 abstract 关键字,抽象类也就无须像 Java 那样添加 abstract 修饰了。
4.4 全纯虚函数
如果一个类中全部都是纯虚函数,那么这个类就相当于 Java 的接口(C++ 中没有接口的概念):
class Student {
int _id;
string name;
int age;
};
// 此类所有的函数,都是纯虚函数,就相当于 Java 的接口了
class IStudent_DB {
virtual void insertStudent(Student student) = 0;
virtual void deleteStudent(int _id) = 0;
virtual void updateStudent(int _id, Student student) = 0;
virtual Student queryByStudent(Student student) = 0;
};
// Java 的实现类
class Student_DBImpl1 : public IStudent_DB {
public:
void insertStudent(Student student) {
// 插入操作,省略代码...
}
void deleteStudent(int _id) {
// 删除操作,省略代码...
}
void updateStudent(int _id, Student student) {
// 更新操作,省略代码...
}
Student queryByStudent(Student student) {
// 查询操作,省略代码...
}
};
int main() {
Student_DBImpl1 studentDbImpl1;
return 0;
}
5、其他
这一节会介绍一些实用的小知识点。
5.1 对象成员变量初始化的方式
类中的成员变量如果是对象类型,编译器会要求必须在构造函数中初始化。根据成员的内构造函数的情况,分为两种情形:
-
如果成员类内有空参构造函数(没有显式声明任何构造函数隐式会有一个,或者声明了其他构造函数,但是显式声明了空参构造函数),持有该成员的类内可以不用显式为该成员做初始化,编译器会自动调用该成员的空参构造函数为其初始化:
class Person { protected: // string 实际就是对 char* 的封装,它是 std 空间内的成员, // 源码的常用写法是 std::string,而不会导入命名空间 string name; int age; public: Person(string name, int age) : name(name), age(age) {} }; class Course { private: string name; }; class Student : public Person { private: // Course 是一个对象类型的成员变量,必须在构造函数内初始化 Course course; public: Student(string name, int age) : Person(name, age) {} };
Student 持有 Course 对象,由于 Course 仅有一个默认的隐式的空参构造函数,因此在 Student 内编译器会自动调用该构造函数为 Course 进行初始化,所以上述代码编译可以通过。
-
如果成员类中没有空参构造函数(即显式声明了其他构造函数,但没有显式声明空参构造函数),编译器要求在构造函数中必须为该成员进行初始化,否则编译报错:
class Person { protected: string name; int age; public: Person(string name, int age) : name(name), age(age) {} }; class Course { private: string name; public: // Course(); Course(string name) : name(name) {}; };
现在 Course 内仅有一个单参的构造函数,空参构造函数没有显式声明(注释没打开),再看 Student 内构造函数的几种形式:
class Student : public Person { private: Course course; public: // 1.采用原来的声明方式不行了,因为没有为 course 初始化 /*Student(string name, int age) : Person(name, age) {}*/ // 2.这种方式也不行,因为编译器检测不到你是否真的给 course 初始化了 /*Student(string name, int age, Course course) : Person(name, age) { this->course = course; }*/ // 3.这种方式可以,直接用参数传入的 course 为成员变量的 course 初始化了 Student(string name, int age, Course course) : Person(name, age), course(course) { } // 4.这种方式也可以,将参数上的 courseName 作为 Course 构造函数的参数,通过 // Course 构造函数为 course 初始化 Student(string name, int age, string courseName) : Person(name, age), course(courseName) { } };
5.2 回调
使用 Java 的代码模式在 C++ 中实现一个回调,以登录操作为例:
class ResponseData {
public:
string data;
ResponseData(string data) : data(data) {}
};
// 登录的回调接口
class ILoginCallback {
// 接口内的默认可见度是 private,需要声明为 public 才可由子类重写
public:
virtual void onLoginSuccess(int code, string message, ResponseData responseData) = 0;
virtual void onLoginFailure(int code, string message) = 0;
};
class LoginCallbackImpl : public ILoginCallback {
void onLoginSuccess(int code, string message, ResponseData responseData) {
cout << "登录成功,返回的数据:" << responseData.data << endl;
}
void onLoginFailure(int code, string message) {
cout << "登录失败,错误码与错误信息:" << code << "," << message << endl;
}
};
/**
* 登录函数,注意第三个参数不能直接传 ILoginCallback 的对象,因为它内部有纯虚函数
* 没有实现,相当于一个接口,不能实例化,也就不能传这个类型的对象
*/
void login(string name, string pwd, ILoginCallback &iLoginCallback) {
if (name.empty() || pwd.empty()) {
cout << "用户名或密码为空!" << endl;
return;
}
// 模拟服务器验证账号密码并回调相关函数
if (name == "Tracy" && pwd == "123456") {
iLoginCallback.onLoginSuccess(200, "Login success!", ResponseData("恭喜登录成功!"));
} else {
iLoginCallback.onLoginFailure(250, "用户名或密码错误!");
}
}
int main() {
LoginCallbackImpl loginCallback;
login("Tracy", "123456", loginCallback);
login("Tracy", "123", loginCallback);
}
测试结果:
登录成功,返回的数据:恭喜登录成功!
登录失败,错误码与错误信息:250,用户名或密码错误!
5.3 模板函数
C++ 的模板函数类似于 Java 的泛型:
template<typename T>
T add(T number1, T number2) {
return number1 + number2;
}
int main() {
// 12 + 345 = 357
cout << "12 + 345 = " << add(12, 345) << endl;
// 12.67 + 34.11 = 46.78
cout << "12.67 + 34.11 = " << add(12.67, 34.11) << endl;
}
与 Java 泛型类似,在方法名后面可以通过 <>
指定模板中定义的类型,如 add<int>(12, 345)
,当然,从示例中能看出,这个类型是可以省略不写的。
5.4 函数对象类模板
C++ 提供了函数对象类模板:
void sample1() {
plus<int> add_int;
cout << add_int(1, 2) << endl;
plus<string> add_string;
cout << add_string("ABC", "DEF") << endl;
plus<float> add_float;
cout << add_float(1.34, 2.56) << endl;
}
仿照源码可以自定义这种函数,先看源码:
template<typename _Tp>
struct plus : public binary_function<_Tp, _Tp, _Tp>
{
/// Returns the sum
_GLIBCXX14_CONSTEXPR
_Tp
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x + __y; }
};
自定义实现:
template<typename T>
struct custom_plus : public binary_function<T, T, T> {
T operator()(const T &x, const T &y) const {
return x + y;
};
};
void sample2() {
custom_plus<int> add_int;
cout << add_int(1, 2) << endl;
custom_plus<string> add_string;
cout << add_string("ABC", "DEF") << endl;
custom_plus<float> add_float;
cout << add_float(1.34, 2.56) << endl;
}