Skip to content

C++ 基础

Published: at 11:30 AM

new delete

在C++ 中可以使用关键词 new 和 delete 进行动态内存的管理。

// 几种方法都可以
int *p1 = new int(3);
int *p2 = new int[2];
int *p3 = new int;
int *points = new Point[4];
// 对于对象数组来说
delete points // 代表用来释放内存,且只用来释放points指向的内存。
delete[] points // 逐一调用数组中每个对象的析构函数。

// 但对于内置类型来说,下面两个效果是一样的。
delete p2
delete[] p2

引用

引用作为函数形参

联想到 c 语言的swap 函数

void swap(int a, int b) {
    int t = a;
    a = b;
    b = t;
}

void main() {
    int a = 2, b = 3;
    swap(a, b);
}

这个函数是不会真正交换 main 函数中的变量 a, b的值的。如果要实现交换值就要使用指针。

void swap_x(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = a;
}

void main() {
    int a = 2, b = 3;
    swap(&a, &b);
}

但是也发现了仅仅实现这么简单的功能也要使用指针,太麻烦了。 在C++ 中利用引用可以解决这个问题。

void swap(int &a, int &b) {
    int t = a;
    a = b;
    b = t;
}

int main() {
    int a = 2, b = 3;
    swap(a, b);
}

这就是引用的最简单的应用。

引用作为函数返回值

当函数返回值是引用时,函数也就能够作为左值。 具体看代码:

const int maxn = 1000;
int a[maxn]; // 为了简化代码直接使用了全局变量

int& at(int i) {
    return a[i];
}

// 一个错误的示例
int at_wrong(int i) {
    return a[i];
}

void test() {
    at(0) = 1;
    at(1) = 6;
    at(2) = 4;
    at(3) = 8;
    at(4) = 8;

    // 下面一行代码会报错。因为 int 是不能作为左值的。
    at_wrong(5) = 1;
}

引用作为函数返回值这个 很重要。比如:自定义一个数据结构的时候, 需要提供接口,用来修改数据。比如 标准库中的 vector

下面举一个自定义的 类来说明这个特性的重要性。

const unsigned maxn = 10000;

template<class T>
class my_vector {
    T a[maxn];
    unsigned cur = 0;
public:
    void add(T item) {
        a[cur++] = item;
    }

    T& operator [] (int i) {
        return a[i];
    }
};

int main() {
    my_vector<int> a;
    a.add(1);
    a.add(6);
    a.add(4);
    a.add(8);
    a.add(8);
    a.add(3);

    a[6] = 1;
}

重载了 [] 运算符。提供和数组类似的数据访问方式。

const 修饰符

对于指针的 const

对于指针的 const 有一些特殊的地方。下面做一些讨论。 注意:对于引用,并没有这种不同,两种定义的都是,底层 const。

int a = 2;
int b = 3;

const int *p1 = &a;
// 底层(low-level) const
// 不允许指针改变变量的值
// *p1 = 2;
p1 = &b; // 允许

int * const p2 = &a;
// 顶层(top-level) const
// 不允许改变指针的值
// p2 = &b;
*p2 = 3; // 允许

const int * const p3 = &a;
// 都不允许
// p3 = &b;
// *p3 = 3;

对结构体(类)的引用

struct Point {
    int x, y;
};

// 加上const 修饰符保护数据
void test(const Point &point) {
    point.x = 3; // 错误
}

// 重点:这种情况下会发生重载。
// 具体见下一节
void test(Point &point) {
    point.x = 3; // 正确
}

函数特性

内联函数

利用内联函数,相对于正常的函数来说,一个优点就是执行效率会有所提升。 将指定的函数体插入并取代每一处调用该函数的地方。这样会减少调用函数(我认为是 压栈出栈所消耗的时间)的时间吧。

注意:对递归函数的内联扩展可能引起部分编译器的无穷编译。

int max(int x, int y) {
    return x > y ? x : y;
}

// 使用内联函数的 max 函数
inline max_x(int x, int y) {
    return x > y ? x : y;
}

函数重载

重载的条件:同名函数的形式参数(指参数的个数、类型或者顺序)不同。

利用一个函数名去执行不同的功能。

// add 能够加 两个或者三个数。
// 形式参数个数不同
void add(int a, int b, int c);
void add(int a, int b);

请注意下面一种情况:

下面程序表明 const Point & 这个类型和 Point & 是不同的。

struct Point {
    int x, y;
};

// 发生函数重载。const 关键字修饰
void show(const Point &point) {
    cout << "const: " << point.x << "," << point.y << endl;
}

// 调用后将 x 坐标变为 0
void show(Point &point) {
    cout << "not const: " << point.x << "," << point.y << endl;
    point.x = 0;
}

int main() {
    Point point1 = {1, 2};
    const Point point2 = {2, 3};
    show(point1);
    show(point1);
    show(point2);
}

程序运行的结果是。

not const: 1,2
not const: 0,2
const: 2,3

函数参数默认值

使用函数默认值,调用的时候就可以简化传参,有时候也避免了再写一个函数去重载。

// 计算两个数,a ope b。ope 默认为 +
void compute(int a, int b, char ope = '+');

void compute(int a, int b, char ope) {
    ...
}

注意:函数默认值应该在声明的时候写上去。

具有默认值的应该在函数形参列表的最右边。

void add(int a = 0, int b); // 这样子是错的

数据的抽象

类的基本思想是数据抽象和封装。

数据抽象是一种依赖于 接口和实现 分离的编程技术。

接口包括用户所能执行的操作;类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。

封装实现了类的接口与实现的分离。

封装后的类隐藏了它的实现细节,也就是无法通过接口去访问实现部分。

在C++ 中抽象数据类型(abstract data type) 的定义可以用 class 和 struct 来定义。但是他们两个区别仅仅在于 默认的数据访问权限。(struct 默认为 public, class 默认为 private)。

成员函数

先定义一个 Book 类

struct Book {
    std::string name;
    std::string book_no;

    std::string isbn();
}

std::string Book::isbn() {
    return book_no;
};

其中 isbn 成员函数是在类的体内声明,在类的体外定义。Book 类本身是一个作用于,所以定义 isbn 的时候,要加上 Book:: 表示是在 类作用域中。

成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。

// 类似于这样
std::string isbn(Book* this);
class Book {
    std::string name;
    std::string book_no;
public:
    std::string isbn() const {
        return book_no;
    }
};

这里的 isbn 在类的体内声明并且定义,并且加上了一个 const 修饰符。const 修改了隐式 this 指针的类型。所以成员函数中不能修改数据成员。

这个const 也很重要,比如在使用标准库中的 set时应该这么定义,

struct Node {
    int v;
    // 重载 < 运算是必须的。因为 set 内部实现是红黑树,要用到 <
    // 第二个 const 也是必须的。
    bool operator < (const Node &rhs) const {
        return v < rhs.v;
    }
};

// 创建一个 set 类型的 数据。
set<Node> data;

构造函数

注意:C++编译器只有在用户没有定义任何构造函数的时候,才默认生成一个无参的构造函数。

class Data {
    int id;
public:
    // C++ 11 中表示使用默认的构造函数。
    Data() = default;

    // 初始值列表
    Data(int id) : id(id * id) {
        cout << "new data" << endl;
    }

    // 拷贝构造函数
    Date(const Data &data) {
        this->id = data.id;
    }
};

下面主要对拷贝构造函数进行讨论:

// 当调用 test1 时,形参的初始化需要调用拷贝构造函数
void test1(Data d);

// 调用 test2 的过程
// 1. 先传递引用给 d。     --- 不调用拷贝构造函数
// 2. 将 d 的值拷贝给 tmp。--- 调用拷贝构造函数
// 3. 将 tmp 作为返回值返回过去。--- 拷贝构造函数?
// 3. 对于第三步(tmp很特殊) C++ 11 做了优化(RVO)不会进行拷贝。
// Return Value Optimization (RVO)

Data test2(const Data &d) {
    Data tmp = d;
    return tmp;
}

// 毫无疑问 test3 在第三步也是会拷贝的。
// 有兴趣的可以去找关于RVO的资料,详细看看。
Data test3(const Data &d) {
    Data *tmp = new Data(d);
    return *tmp;
}

隐式类类型转换

#include <iostream>
using namespace std;

struct Data {
    string val;
    Data(string v) : val(v) {}

    Data (istream &in) {
        in >> val;
    }
};

void test(Data d) {
    cout << d.val << endl;
}

int main() {
    // 下面 注释掉的是正常的调用构造函数去实例化对象。
    // 大家都知道就不多说。
    // Data d1(cin);
    // Data d2("12");
    // test(d1); test(d2);

    // 这里传入的参数是 istream
    // 而不是 test 的形式参数 Data
    test(cin);

    // 这里传入的参数是 string
    test((string)"abcd");
    // 下面的会报错
    // 因为 要隐式 "abcd" -> string -> Data
    // 要使用两种隐式转换才能转换。所以不行
    // test("abcd");
}

有时候并不需要这种隐式转换,这时候应该在类的构造函数前面加上 explicit 修饰符。(只对一个参数的构造函数有效)

聚合类

聚合类的定义:

比如下面这个:

// 这是一个非常常用的邻接表Edge 的结构体
struct Edge {
    int v, next;
    double d;
};

void test() {
    // 可以这样子进行初始化。
    // 初始值的顺序应该和声明的顺序一致。
    Edge e = {1, 3, 2.2};
}

友元函数

类可以允许其他类或者函数访问他的非公有数据成员,可以在这个类的体内 加上一个用 friend 修饰的类或者函数的声明。

class Test;

class Num {
    // 添加有 friend 的声明。
    friend class Test;
    friend int getNum(Num &num);
    // 友元函数可以在类的内部定义。
    // 但是外部也要有函数的声明。
    /* friend int getNum(Num &num) {
        return num.val;
    }; */

    int val;
public:
    Num(int val) : val(val) {}
};

// 友元类定义
class Test {
public:
    int getNum(Num &num) {
        return num.val;
    }
};

// 也可以在外部
// 注意:友元函数getNum 并不属于Num类。
int getNum(Num &num) {
    return num.val;
};

#include <iostream>

using namespace std;

int main() {
    Num num(1);
    Test t;
    cout << t.getNum(num) << endl;
    cout << getNum(num) << endl;
}

静态成员

可以在成员前面加上 static 关键字,来声明静态成员。

要注意几点:

用来记录类被实例化几次的示例已经很古老而且没有新意。所以这里举出另一个关于静态成员的应用:

单例设计模式。(私有化构造函数,使得类实例化出的对象只有一个)

#include <iostream>
using namespace std;

struct Node {
private:
    Node(int n) : val(n) {}

public:
    int val;
    // 声明的时候不进行初始化
    // static Node * one = nullptr; // 错误
    static Node * one;
    static Node * instance() {
        if (one == nullptr) {
            one = new Node(0);
        }
        return one;
    }
};

// 一般要在全局这里进行定义,初始化。
Node* Node::one = nullptr;

int main() {
    Node *p1 = Node::instance();
    Node *p2 = Node::instance();
    Node *p3 = Node::instance();
    cout << (p1 == p2) << endl;
    cout << (p2 == p3) << endl;
    // 结果 p1, p2 ,p3 都相等
}

OOP:概述

面向对象程序设计的核心思想是:数据抽象,继承和动态绑定。

派生类构造

注意:派生类构造函数只能初始化它的直接基类。而且必须要在初始化列表中调用基类的构造函数。

struct Base {
    int i;
    Base(int i) : i(i) {}
};

struct Derived1 : public Base {
    int ii;
    Derived1(int i, int ii) :
        Base(i), ii(ii) {
    }
};

struct Derived2 : public Derived1 {
    int iii;
    Derived2(int i, int ii, int iii) :
        Derived1(i, ii), iii(iii) {
    }
};

int main() {
    Derived2 obj(1, 2, 3);
}

虚函数

在C++ 中,基类必须区分两种函数:

对于希望派生类进行重写的函数,在函数定义前面加上 virtual 关键字。 如果基类把函数声明为,虚函数,那么该函数在派生类中隐式 的也是虚函数。

下面举例说明一下。

#include <iostream>
using namespace std;
struct A {
    void test() {
        cout << "HelloA" << endl;
    }
};

struct B : public A {
    void test() {
        cout << "HelloB" << endl;
    }
};

struct C : public B {
    void test() {
        cout << "HelloC" << endl;
    }
};

int main() {
    A a;
    B b;
    C c;
    A *pb = &b;
    A *pc = &c;
    pb->test();
    pc->test();
}

输出结果是:

HelloB
HelloC

如果把virtual 关键字去下来,输出的结果就是

HelloA
HelloA

动态绑定

只有指针或者引用 能实现运行时多态。(重点)

可以将基类的指针或引用绑定到派生类对象上。例如:在上一节例子中,可以用 A& 指向一个 B对象。

#include <iostream>
using namespace std;

struct Base {
    int v;
    virtual void print() {
        cout << "Base" << endl;
    }
};

struct Derived : public Base {
    int vv;
    void print() {
        cout << "Derived" << endl;
    }
};

void test(Base &p) {
    p.print();
}

// 对象不能实现运行时多态
void test_wrong(Base p) {
    p.print();
}

int main() {
    Derived obj;
    test(obj);
    test_wrong(obj);
}

输出结果是:

Derived
Base

静态类型是变量声明时的类型(比如 p 的静态类型是 Base)。动态类型表示的是内存中对象的类型。(运行时才能知道) p的动态类型根据 test 传入的参数不同,动态类型也会改变。

注意:基类指针或者引用的静态类型可能与其动态类型不同。如果既不是指针也不是引用那么动态类型永远与静态类型一致。(上述例子中的test_wrong 函数输出的内容)

抽象基类

在声明纯虚函数的时候在后面 书写 =0 就可以把虚函数定义为 纯虚。 就可以在类定义的时候,不定义这个虚函数。(让派生类去定义)。

含有纯虚函数的类是抽象基类。(不能 实例化抽象基类)

#include <iostream>
using namespace std;

struct Base {
    int i;
    virtual void pure_virtual_fun() = 0;
};

struct Derived : public Base {
    void pure_virtual_fun() {
        cout << "HelloWorld" << endl;
    }
};

int main() {
    // 不能定义抽象基类
    // Base b; // 错误

    Derived obj;
    Base *p = &obj;
    p->pure_virtual_fun();
}

一个应用

下面是一个简单工厂模式。是面向对象编程中的一种重要设计模式。 有兴趣的可以把这个程序改成抽象工厂模式。

#include <iostream>
using namespace std;

class Book {
protected:
    string name;
    string isbn;
public:
    virtual void show() = 0;
};

class AdventureBook : public Book {
public:
    void show() {
        cout << "this is a adventure book." << endl;
    }
};

class ScienceBook : public Book {
public:
    void show() {
        cout << "this is a science book." << endl;
    }
};

enum BookType {
    ADVENTURE,
    SCIENCE
};

class Factory {
public:
    Book * create(BookType type) {
        switch (type) {
            case BookType::ADVENTURE :
                return new AdventureBook();
            case BookType::SCIENCE :
                return new ScienceBook();
            default:
                return nullptr;
        }
    }
};

int main() {
    Factory factory;
    Book *book1 = factory.create(BookType::ADVENTURE);
    Book *book2 = factory.create(BookType::SCIENCE);

    book1->show();
    book2->show();
}

[1] Lippman S B. C++ Primer[M]. Pearson Education India, 2005.

[2] 关于RVO和std move的一篇文章

[3] new_and_delete