NDK | C++ 复习笔记

2,392 阅读6分钟

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)


基础概念

  • 命名空间
声明命名空间:
namespace xurui {
    int age = 1;
    char * name = "xurui";
}

使用命名空间(要求放在声明之后,可以在文件头调用,也可以在函数内调用):
using namespace xurui;

访问命名空间内的成员:
xurui::name 或 name(没用冲突时,可以省略命名空间::)
#include<iostream> // C++ 标准库
using namespace std; // 命名空间,类似于 Java 的内部类

cout<< "C++" << endl; // 引用引用了std::命名空间,所以可以省略

提示: << 是操作符重载

  • C++ 中的 bool 类型

C 语言没有布尔类型,C++ 中的 bool 类型其实是对 int 的包装。

cout << true << endl;
输出:1
  • C 与 C++ 中常量的区别
C:假常量
const int number = 100;
int * numP = &number;
*numP = 10000;
printf("%d\n", number); // 10000,“常量了” 被修改

C++:真常量
const int number = 100;
// 有的编译器不通过,有的可以通过但运行报错
int * numP = &number;
  • 引用的原理

引用变量就是一个另一个变量别名,一旦把引用初始化为某个变量,就可以使用该引用名称来访问变量。

引用 vs 指针

引用很容易与指针混淆,它们之间有三个主要的不同:

  • 1、不存在空引用,引用必须连接到一块合法的内存。存在野指针 / 悬空指针,野指针(wild pointer)是未初始化过的指针,悬空指针(dangling pointer)是指向已经被释放的内存的指针;
  • 2、引用必须在创建时被初始化,指针可以在任何时间被初始化;
  • 3、一旦引用被初始化为一个对象,就不能被指向到另一个对象,指针可以在任何时候指向到另一个对象。
  • 默认形参
int add(int n1 = 0, int n2 = 1) {
}

默认形参可以在函数声明是指定,也可以在函数定义时指定。

  • 可变参数
描述
va_list参数列表指针 arg_ptr
va_start(arg_ptr, argN)使参数列表指针 arg_ptr 指向函数参数列表中的第一个可选参数,argN 是位于第一个可选参数之前的固定参数
va_arg(arg_ptr, type)返回参数列表中指针 arg_ptr 所指的参数, 返回类型为 type. 并使指针 arg_ptr 指向参数列表中下一个参数
va_end(arg_ptr)清空参数列表, 并置参数指针 arg_ptr 无效
#include <stdarg.h>

using namespace std;

void sum(int count, ...) {
    va_list vp;
    va_start(vp, count);

    for (int index = 0; index < count; index++) {
        int number = va_arg(vp, int);
        cout << number << endl;
    }

    va_end(vp);
}

int main() {
    // 输出:
    // 1
    // 2
    // 3
    sum(3, 1, 2, 3);

    return 0;
}
  • 函数重载: 与 C 语言不同,C++ 支持函数重载

需要注意,如果你定义了带默认形参的函数,需要确认没有参数更少的重载函数,否则会报错:Call to 'add' is ambiguous。例如:

int add(int n1){
    return n1;
}

int add(int n1 = 1,int n2 = 1){
    return n1 + n2;
}

int main(){
    add(1); (X)// Call to 'add' is ambiguous
}
  • 运算符重载:

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

Student .h:
class Student {
public:
    Student operator+(Student student);
};

Student .cpp:
Student Student::operator+(Student student) {
    return Student();
}

----------------------------------------------
更严格的写法:
class Student {
public:
    Student operator+(const Student &student);
};

注意:如果没有使用 &,函数调用时会额外构建一个对象副本(拷贝构造函数)
Student Student::operator+(const Student &student) {
    return Student();
}

类与对象

  • 类声明的规范

使用头文件 .h 声明类成员,使用 .cpp 实现类成员。这是 C++ 类定义的规范。目的是在公开 so 库时,只需要向客户端提供 .h 头文件就可以调用 so 库内的实现。

  • 声明类

Student.h

class Student {
// 私有成员
private:
    char*name;
    int age;
};
  • 实现类

Student.cpp

class Student {
public:
    char *getName() const;

    void setName(char *name);

    int getAge() const;

    void setAge(int age);
// 私有成员
private:
    char*name;
    int age;
};

Student.cpp

char *Student::getName() const {
    return name;
}

void Student::setName(char *name) {
    Student::name = name; // 或 this->name = name;
}

int Student::getAge() const {
    return age;
}

void Student::setAge(int age) {
    Student::age = age; // 或 this->age = age;
}

提示:Student.cpp 编辑页面按下Alt+insert,可以选择自动插入 setter/getter 等函数

  • 对象的静态开辟和动态开辟
静态开辟:栈
Student student; // 未调用构造函数
student.setAge(10);
student.setName("mark");
cout << "name:" << student.getAge() << ",name:" << student.getName() << endl;

Student student2("Amy") // 会调用构造函数
// 函数出栈是会隐式调用 delete

动态开辟:堆
Student *student = new Student(); // 会调用构造函数
student->setAge(10);
student->setName("mark");
cout << "name:" << student->getAge() << ",name:" << student->getName() << endl;

if (student) {
    delete student; // new 分配的空间用 delete
    student = NULL; // 防止存现悬空指针
    // free(student); // (X) malloc 分配的空间用 free
}

注意: new 分配的空间用 delete,malloc 分配的空间用 free。malloc 不会调用构造函数,free 不会调用析构函数。

  • 构造函数 & 析构函数
声明
class Student {
public:
    Student();
    Student(char*name);
    ~Student();
};
----------------------------------------------
// 调用另一个重载构造函数
Student::Student() : Student("mark") {
}

Student::Student(char *name) {
    this->name = (char *) (malloc(sizeof(char) * 10));
    strcpy(this->name, name);
}

Student::~Student() {
    // 必须释放构造器中开辟的堆区成员
    if (this->name) {
        free(this->name);
        this->name = NULL;
    }
}

提示: new 会调用构造函数,delete会调用析构函数。

  • 拷贝构造函数

1、当类中没有定义任何构造函数时,编译器会默认提供一个无参构造函数且其函数体为空; 2、当类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,进行成员变量之间的拷贝。(这个拷贝操作是浅拷贝)。 3、调用一次构造函数,调用一次默认拷贝构造函数(浅拷贝),两个对象的指针成员所指同一块内存。调用两次析构函数,内存空间重复释放导致奔溃。

情况 1:指针赋值,没有调用拷贝构造函数
Student* student1 = new Student();
Student* student2 = student1;

情况 2:隐式调用拷贝构造函数
Student student1;
Student student2 = student1; 

情况 3:不会调用自定义的拷贝构造函数,但会调用默认拷贝构造函数
Student student1;
Student student2; 
student2 = student1; 

提示: struct 也有类似的浅拷贝。

  • 静态成员 1、可以使用 类名:: 直接访问静态成员(字段/函数) 2、静态字段需要声明与实现; 3、静态成员只能访问静态成员。

Student.h

class Student {
public:
    1、先声明静态成员:
    static int id;
    static void update(){
        id = 2;
    }
};

Student.cpp

2、再实现静态成员(必须):
int Student::id = 1;
Student student;
cout << student.id << endl;
Student::update();
cout << student.id << endl;

输出:
1
2
  • 友元函数

1、友元函数是在类中声明,在类外部实现的函数; 2、友元函数不是成员函数,但是它可以访问类中的私有成员; 3、友元函数一定程度上破坏了类的封装性。

Student.h

class Student {
public:
    friend void setAge(Student * student,int age);
private:
    int age;
};

main.cpp

void setAge(Student *student, int age) {
    student->age = age;
}

int main() {
    Student student;
    setAge(&student, 1);

    // 输出:
    // 1
    cout<<student.getAge()<<endl;

    return 0;
}
  • 友元类 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
class ImageView {
private:
    int viewSize;

    friend class Class; // 友元类
};

class Class {
public:
    ImageView imageView;

    void changeViewSize(int size) {
        imageView.viewSize = size; // 如果不声明友元类 “friend class Class”,无法访问
    }
};

思考: 在 Java 中,私有成员无法直接访问,但是使用 Class#getDeclareField() 却可以访问,底层原理是不是 C++ 友元类?

  • 继承
class Rectangle: public Shape, public PaintCost
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};

多继承有二义性,会导致程序不够健壮,所以 Java 的设计里面中不支持多继承。在 C++ 中,因为存在多继承,所以在开发过程中要主动避免程序二义性。如果已经出现二义性的场景,有两种解决方案: 1、可以使用类名::变量名显性访问来规避二义性; 2、使用虚继承;

  • 继承类型

当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。使用 public 继承,不同继承类型规则:

1、公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。 2、保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。 3、私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

  • 虚继承

为了解决多继承时的命名二义性和冗余数据问题,C++ 设计出虚继承,使得在派生类中只保留一份间接基类的成员。

// 间接基类A
class A{
protected:
    int m_a;
};
// 直接基类B
class B: virtual public A{  // 虚继承
protected:
    int m_b;
};
// 直接基类C
class C: virtual public A{  // 虚继承
protected:
    int m_c;
};
  • 多态

C++ 默认关闭多态(或者说是静态多态 / 静态链接):函数调用在程序执行前就确定了。如果要开启多态,需要在 父类 函数上声明virtual(声明为虚函数)。

  • 纯虚函数

C++ 接口没有直接的抽象类 / 接口的概念,但是可以使用纯虚函数来达到类似的语义。纯虚函数声明如下:

virtual void funtion1()=0;

纯虚函数用来规范派生类的行为,即接口,纯虚函数一定没有定义。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。


模板(类似泛型)

  • 函数模板
template <class T> void swap(T& a, T& b){
}
  • 类模板
template<class T> class A{
public: 
T a; 
T b; 
T hy(T c, T &d);
};

STL 容器

描述
vector动态数组
list链表
stack
queue队列
set红黑树,自动排序,无重复
multiset红黑树,自动排序,有重复
map红黑树,有序的键 / 值对,有重复
multimap红黑树,有序的键 / 值对,无重复

参考资料

  • 菜鸟教程
  • 《C++ Primer Plus》—— [美] Stephen Prata 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!