C语言学完之后赶紧来学C++,学完之后打算自己来做一个桌面应用

一、原理和概念的介绍

C++是如何运行的?

所有的.cpp文件都会被编译,而头文件不会。头文件通过预处理器(preprocessor)include(内容复制)到.cpp文件中。 每个.cpp文件都会被单独编译成一个.obj(二进制)文件。linker(链接器)负责将每个.obj文件联系起来,组成一个exe。

二、对比C语言后新的收获

头文件

声明和定义

在头文件中,只允许出现声明

其中:声明包括以下几种:

  1. 外部变量 extern修饰的变量
  2. 函数原型
  3. class/struct的声明

如果你在头文件中定义了一个变量,且在多个.cpp文件中又引入了这个头文件。则linker会报重复定义的错误。

头文件保护符

#include引号和方括号的区别

  • 引号通常表示引入一个相对位置下的文件
  • 方括号通常表示引入一个标准库

C语言的标准库通常带.h拓展名,而C++的标准库一般不带

#pragma once

试想以下情况: 当我们重复引入同一个头文件会发生什么情况?试看以下代码

// log.h
void loging(char *str);
struct Player
{
    int age;
};

// header.c
#include <stdio.h>
#include "log.h"
#include "log.h"
int main(void)
{
    char *a = "a";
    loging(a);
}
void loging(char *str)
{
    printf("%s", str);
}

Flower and water

没错,你会看到一个重复定义的报错。如何解决这个问题呢? 我们可使用 #pragma once 指令,告诉编译器对于这个头文件只处理一次

#pragma once
void loging(char *str);
struct Player
{
    int age;
};

当然你可以使用条件编译

#ifndef LOG_H
#define LOG_H

void loging(char *str);
struct Player
{
    int age;
};
#endif

流程控制语句(continue,break,return)

在循环中有以下的用法:

  1. continue 跳出本轮循环
  2. break 跳出本次所有循环
  3. return 跳出本次所有循环,且其后的所有代码都不会执行

指针

  • 指针是一个整数,一个数字,它存储一个内存地址
  • void指针,意味着我们不关心这个数据是什么类型,我们只想存放一个内存地址

内联函数

概念

以inline修饰的函数叫做内联函数, 编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。 (看到在加粗部分时,小伙伴肯定会想,这和c语言中的宏是不是很像了?)

Flower and water

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用

Flower and water

特性

  • inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长/递归的函数不适宜使用作为内联函数。

  • inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内代码比较长/递归等等,编译器优化时会忽略掉内联。

  • inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

Flower and water

三、refence引用和指针的对比

引用:

可以在某个变量类型后面加上个&,表示对某个变量的引用,相当于typedef。

#include <iostream>


int main(void)
{
    int a = 5;
    int &ref = a;
    ref = 6;
    std::cout << ref << std::endl; // 6

}

引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

  1. 引用在定义时必须初始化,指针没有要求。
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
  3. 没有NULL引用,但有NULL指针。
  4. 在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
  5. 引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。
  6. 有多级指针,但是没有多级引用。
  7. 访问实体的方式不同,指针需要显示解引用(&),而引用是编译器自己处理。引用比指针使用起来相对更安全。

四、class 类

4.1如何书写一个class

  1. 一个class通常由头文件.h声明和源文件.cpp分开书写。
  2. class的声明和原型都要放到头文件中。
  3. 函数的实体部分放到源文件.cpp中。

4.2成员变量

4.2.1.本地变量

在函数内部定义的变量,其作用域只在函数内有效

4.2.2.成员变量、参数、本地变量

成员变量

  • 定义在构造函数和方法之外
  • 其生存周期和实例对象一样
  • 作用域和class作用域一样,所以在构造函数和成员方法内部都可以访问这个变量
  • 在class内部声明的成员变量,其实是个外部变量
#include <iostream>


class A
{
private:
    int i; // 相当于 extern int i ;


public:
    void f();
};

void A::f()
{
    int j = 10;
    i = 11;
}
int main(void)
{
    A a;
    a.f();
}

4.2.3.成员变量和成员方法

成员方法是属于类的,而成员变量是属于每个实例对象的。

如上,用一个类生成多个实例,并调用成员方法改变实例里的成员变量。 成员方法是如何知道具体是哪个实例呢?

class A
{
private:
    int i;

public:
    void f();
};

void A::f()
{

    i = 11;
    std::cout << i << std::endl;
}
int main(void)
{
    A a;
    A a1;
    a.f(); 
    a1.f(); // 调用类的方法,如何修改实例?

}

在class中调用成员方法的时候会传入一个 隐藏的参数this 。this指向当前实例

4.3构造和析构

构造函数

  • 构造函数会在实例化的时候自动调用
  • 构造函数名必须与类名相同
  • 构造函数没有任何返回值和返回类型
  • 构造函数可以带参数,且可以有默认值
#include <iostream>


class Person
{
public:
    Person(int defaultAge = 10, int defaultHeight = 170);
    void show();
    ~Person();

private:
    int age;
    int height;
};

Person::Person(int defaultAge, int defaultHeight)
{
    std::cout << defaultAge << " before construct " << defaultHeight << std::endl;
    age = defaultAge;
    height = defaultHeight;
}
Person::~Person()
{
    std::cout << " before construct age = " << age << " ; height = " << height << std::endl;
}
void Person::show()
{
    std::cout << "age=" << age << std::endl;
    std::cout << "height=" << height << std::endl;
}

int main(void)
{
    Person p1;
    Person p2(12);
    Person p3(21, 178);
    p1.show();
    p2.show();
    p3.show();
}

析构

析构是一种特殊的成员函数,当对象的生命周期结束时,用来释放分配给对象的内存空间,并做一些清理的工作

  • 析构函数名与类名必须相同。
  • 析构函数名前面必须加一个波浪号(tide) ~ 。
  • 没有参数,没有返回值,不能重载。
  • 一个类中只能有一个析构函数。
  • 没有定义析构函数,编译系统会自动为和这个类生成一个默认的析构函数

4.4动态内存分配 new & delete

相当于C语言中的malloc和free

new到底干了什么?

  1. 分配内存空间
  2. 调用构造函数

delete到底干了什么?

  1. 调用析构函数
  2. 收回内存空间

4.5类的访问限制

主要有以下三个关键字来控制class的成员属性和方法的访问权限

  • public 任何人都可以访问
  • private 只能通过成员方法访问成员变量/函数
  • protected 只有这个类型本身和其子类能访问

4.6友元

设想一下,你在一个class中声明了private属性。但是外部函数就要访问你这些私有变量。 怎么办?

我们可以声明一个’朋友’,授权给他这些访问权限

注意点:

  1. 友元的概念是针对类外部函数(包括全局函数和其他类的成员函数)而言的, 类自身的成员函数可以自由访问自己类中的成员。
  2. 但是不能把别的类的私有函数定义为友元。
  3. 友元函数不是该类的成员函数
  4. 友元函数中访问类的成员,不能直接写成员的名字,而是要借助类名来访问。
  5. 友元函数和类可以访问类中的所有成员变量,包括public,private,protected.
  6. 因为友元函数不是成员函数,也就没有this指针,所以其参数必须有一个为类对象。
  7. 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
#include <iostream>


using namespace std;

class Player
{
private:
    int gender;
    bool is_over_28;

public:
    Player(int gender = 0, bool is_over_28 = true);
    friend void myFriend(Player &player);
};

Player::Player(int gender, bool is_over_28)
{
    this->gender = gender;
    this->is_over_28 = is_over_28;
};

void myFriend(Player &player)
{
    cout << "form friend gender = " << player.gender << endl;
    cout << "form friend is_over_28 = " << player.is_over_28 << endl;
}

int main(void)
{
    Player player(13, true);
    myFriend(player);
}

4.7class和struct的对比

  • class的成员属性默认是private,struct的成员默认是public。
  • class有构造函数和析构函数,和struct没有。

4.8初始化列表

通常情况下,我们会在构造函数中这样给变量赋初始值。

class Player
{
private:
    int x, y;

public:
    Player()
    {
        this->x = 10;
        this->y = 12;
    }
};

但是现在有一种更精简的写法:

class Player
{
private:
    int x, y;

public:
    Player(int xa = 10, int ya = 12) : x(xa), y(ya){};
};

4.8.1 初始化和赋值的对比

Player::Player(int xa = 10) : x(xa){};

这种写法叫做初始化,执行时机在调用构造函数之前

Player::Player(int xa = 10) { x = xa };

这种写法叫做赋值,执行时机在调用构造函数内部

4.9 继承

#include <iostream>


using namespace std;

class A
{
private:
    int x;

public:
    A(int ii) : x(ii) { cout << "A::A()" << endl; }
    ~A() { cout << "A::~A()" << endl; }
    void print() { cout << "A::print()  " << endl; }
    void print(int i) { cout << " A::print(int i ) " << i << endl; }
    void set(int x)
    {
        this->x = x;
        cout << "after set x = " << this->x << endl;
    }
};

class B : public A
{
public:
    B() : A(15) { cout << "B::B()" << endl; };
    ~B() { cout << "~B::B()" << endl; }
    void print(int i) { cout << "B::print()  " << endl; }
};

int main(void)
{
    B b;
    b.print(10); // B::print()

    b.set(23);
    cout << "after set x = main" << b.x << endl; // 成员 "A::x" (已声明 所在行数:8) 不可访问C/C++(265)

}

和javascript的类有以下几点区别:

  • 自动调用父类的构造和析构函数,不需要super()。
  • 子类无法访问父类的private属性。
  • 子类若想调用父类的构造函数,则必须在初始化列表里面做
  • 子类和父类的构造析构函数调用顺序如下:父构造 -> 子构造 -> 子析构 -> 父析构
  • name-hiding 当子类和父类存在相同的属性方法,子类会覆盖父类(直接替换)

4.10 类中的const

  • 若用const修饰一个实例对象,则这个class中必须要有一个构造函数。
class A
{
    int i;
public:
    void f() { cout << "f()" << endl; }
    void f() const { cout << "f() const" << endl; }
    // A(){};
};

int main(void)
{
    A const a;
    // error 常量 变量 "a" 需要初始值设定项 -- 类 "A" 没有用户提供的默认构造函数C/C++(811)
    a.f();  // f() const
}
  • 当前存在相同名称的成员方法的时候,用const修饰的成员方法优先级更高。
  • 如果成员变量用const修复,则必须通过初始化列表初始化这个变量。

4.11 多态性与虚函数

4.11.1 多态性(polymorphism)

调用同一个函数名,根据需要实现不同的功能。

  • 编译时的多态性:静态多态(函数重载、运算符重载)
  • 运行时的多态性:动态多态(虚函数)

函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时就能决定调用的是哪个函数。静态多态性又称编译时的多态性。静态多态是通过函数重载、运算符重载实现的。

运行时的多态性是指在程序执行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况来动态地确定。动态多态是通过虚函数(virtual function)实现的。

4.11.2 虚函数

C++中的虚函数就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数,而实现不同功能的函数。

#include <iostream>
#include <string>

using namespace std;

class Entity
{
public:
    std::string GetName() { return "Entity"; }
};

class Player : public Entity
{
private:
    std::string m_Name;

public:
    Player(const std::string &name) : m_Name(name) {}
    std::string GetName() { return m_Name; };
};

int main(void)
{
    Entity *e = new Entity;
    cout << e->GetName() << endl;

    Player *p = new Player("Winston");
    cout << p->GetName() << endl;

    Entity *entity = p;
    cout << entity->GetName() << endl; // entity 
}

如上,我们声明了一个父类的指针,赋值为一个子类实例。当调用GetName方法的时候, 我们理所当然的认为肯定是调用在子类上面的方法。实际却调用的是父类的方法。 若想改变这种行为,我们该怎么办呢?

class Entity
{
public:
    virtual std::string GetName() { return "Entity"; }
};

class Player : public Entity
{
private:
    std::string m_Name;

public:
    Player(const std::string &name) : m_Name(name) {}
    virtual std::string GetName() override { return m_Name; };
};

int main(void)
{
    Entity *e = new Entity;
    cout << e->GetName() << endl;

    Player *p = new Player("Winston");
    cout << p->GetName() << endl;

    Entity *entity = p;
    cout << entity->GetName() << endl; // Winston
}

答案就是虚函数,它会维护一个虚函数列表,在运行的时候自动去分配执行子类的成员方法。

4.12 纯虚函数

C++中的纯虚函数的本质上犹如其他语言中的抽象方法

纯虚函数允许我们定义一个在基类中没有实现的函数,然后迫使在子类中实际实现。

#include <iostream>
#include <string>

using namespace std;

class Entity
{
public:
    virtual std::string GetName() = 0;
};

class Player : public Entity
{
private:
    std::string m_Name;

public:
    Player(const std::string &name) : m_Name(name) {}
    virtual std::string GetName() override { return m_Name; };
};

int main(void)
{
    Entity *e = new Entity;
    cout << e->GetName() << endl;

    Player *p = new Player("Winston");
    cout << p->GetName() << endl;

    Entity *entity = p;
    cout << entity->GetName() << endl;
}

warn

在基类中定义某个成员方法 = 0,这就表明这个方法是个抽象方法。

 virtual std::string GetName() = 0;

纯虚函数有以下几个特点:

  1. 父类将不能在被实例化(因为没有抽象方法)。
  2. 要实例化父类,必须在子类中实现抽象方法。

参考链接: