学习C++时遇到的与其它编程语言中的不同与难点

C++是目前公认的比较难学的一门编程语言,因为其中的知识点过于复杂和繁琐,连谷歌设计Golang的时候,都将目标设定为:“不要像C++。”

那么既然这么难学,为什么还要学它呢,是因为它在某些领域拥有非常大的优势:

  1. 游戏服务端开发
  2. 游戏开发
  3. 数字图像处理
  4. 科学计算
  5. 网络软件
  6. 分布式应用
  7. 操作系统
  8. 嵌入式系统
  9. 教育与科研

尤其是现在虚幻引擎在世界范围内逐渐升温,Cocos2d-x也需要使用C++进行开发。所以C++的应用范围非常的广泛,其实编程语言没有什么孰好孰坏之分,只有合适与不合适的区别。

本文的运行环境为32位操作系统,C++开发默认运行环境即为32位操作系统,下面文章中所提到的占用内存空间统统都指的32位运行环境。

1. 指针(非常重要)

目前学习C++时遇到的和其它编程语言最大的不同点,就是指针。指针可能单独作为一部分来看并不难,难点是后面的值传递,地址传递,引用传递,深拷贝,浅拷贝这些知识点使用到指针的时候,就感觉突然从1+1=2的题目上升到微积分。

指针的作用: 可以通过指针间接访问内存

  • 内存编号是从0开始记录的,一般用十六进制数字表示
  • 可以利用指针变量保存地址

指针变量定义语法: 数据类型 * 变量名;

  • 普通变量存放的是数据,指针变量存放的是地址
  • 指针变量可以通过” * “操作符,操作指针变量指向的内存空间,这个过程称为解引用

1.1 指针所占内存

在不同的系统下指针所占的内存不同,一般情况下所有指针类型在32位操作系统下是4个字节,在64位系统下使8个字节。

1.2 空指针和野指针

空指针:指针变量指向内存中编号为0的空间

用途:初始化指针变量

注意:空指针指向的内存是不可以访问的

int main() {

    //指针变量p指向内存地址编号为00000000的空间
    int * p = NULL; 

    //访问空指针报错 
    //内存编号0 ~255为系统占用内存,不允许用户访问
    cout << *p << endl;

    system("pause");

    return 0;
}

野指针:指针变量指向非法的内存空间即指向了系统占用内存空间。

int main() {

    //指针变量p指向内存地址编号为0x1100的空间
    int * p = (int *)0x1100;

    //访问野指针报错 
    cout << *p << endl;

    system("pause");

    return 0;
}

空指针和野指针都不是我们申请的空间,因此不要访问。

1.3 const修饰指针

const修饰指针有三种情况

  1. const修饰指针 — 常量指针
  2. const修饰常量 — 指针常量
  3. const即修饰指针,又修饰常量
int main() {

    int a = 10;
    int b = 10;

    //const修饰的是指针,指针指向可以改,指针指向的值不可以更改
    const int *p1 = &a;
    p1 = &b; //正确
    //*p1 = 100;  报错


    //const修饰的是常量,指针指向不可以改,指针指向的值可以更改
    int *const p2 = &a;
    //p2 = &b; //错误
    *p2 = 100; //正确

    //const既修饰指针又修饰常量
    const int *const p3 = &a;
    //p3 = &b; //错误
    //*p3 = 100; //错误

    system("pause");

    return 0;
}

1.4 指针和数组

因为数组在内存上面开辟的是一块连续的运行空间,而数组的变量也储存的数组所在内存的地址,所以我们可以通过指针来遍历数组。

int main() {

    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    int *p = arr;  //指向数组的指针

    cout << "第一个元素: " << arr[0] << endl;
    cout << "指针访问第一个元素: " << *p << endl;

    for (int i = 0; i < 10; i++) {
        //利用指针遍历数组
        cout << *p << endl;
        p++;
    }

    system("pause");

    return 0;
}

又因为数组的变量名记录的是它的地址,那么问题又来了。

void change(int arr[]) {
    for (int i = 0; i < 10; ++i) {
        arr[i] = 10;
    }
}

int main() {

    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    change(arr);//调用改变函数
    int *p = arr;  //指向数组的指针

    cout << "第一个元素: " << arr[0] << endl;
    cout << "指针访问第一个元素: " << *p << endl;

    for (int i = 0; i < 10; i++) {
        //利用指针遍历数组
        cout << *p << endl;
        p++;
    }

    system("pause");

    return 0;
}

如果我们在函数change中改变了arr的值,那么在main函数中遍历会输出什么结果?

第一个元素: 10
指针访问第一个元素: 10
10
10
10
10
10
10
10
10
10
10

可以看到,在change函数中改变了arr中的值,main函数中的arr也发生了改变。

1.5 指针和函数(非常重要)

这个在其它编程语言中使用时出现的情况也非常的多,虽然其它编程语言中不操作指针,但是涉及到值传递和引用传递,原理上和C++的指针一样。

在一般情况下,形参是不能修饰实参的。例如:

void change(int a) {
    a = 1000;
    cout << "change函数里的a值为" << a << endl;
}

int main() {

    int a = 100;
    change(a);
    cout << "main函数里的a值为" << a << endl;
    system("pause");

    return 0;
}

那么这个函数里的a和函数外的a输出分别是多少?

//change函数里的a值为1000
//main函数里的a值为100

可以看到,即便在change函数里面修改了a的值,但main函数中的a值不会发生改变。

那么下面的例子change函数里的a和main函数里的a输出分别又是多少?

void change(int *a) {
    *a = 1000;
    cout << "change函数里的*a值为" << *a << endl;
}

int main() {

    int a = 100;
    change(&a);
    cout << "main函数里的a值为" << a << endl;
    system("pause");

    return 0;
}

可以看到上面只将主函数中的变量a的地址传入到change函数中,那么运行后主函数中a的值又是多少?

change函数里的*a值为1000
main函数里的a值为1000

可以看到main函数里的a值被改变了。

总结:如果不想修改实参,就用值传递,如果想修改实参,就用地址传递。

1.6 指针总结

无论在哪门编程语言中,指针的指向都是非常重要必须要进行理解的,建议多看看相关类型的资料,因为对于初学者而言,指针并不是那么容易理解,必须在经过大量的练习后,才能融会贯通。

2. 引用

通过引用可以给变量起别名。

语法:数据类型 &别名 = 原名

2.1 注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变

2.2 引用做函数参数

上文也提到过参数有3种传递方式:

  1. 值传递
  2. 地址传递
  3. 引用传递

作用:函数传参时,可以利用引用的技术让形参修饰实参

优点:可以简化指针修改实参

//1. 值传递
void mySwap01(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

//2. 地址传递
void mySwap02(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

//3. 引用传递
void mySwap03(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {

    int a = 10;
    int b = 20;

    mySwap01(a, b);
    cout << "a:" << a << " b:" << b << endl;

    mySwap02(&a, &b);
    cout << "a:" << a << " b:" << b << endl;

    mySwap03(a, b);
    cout << "a:" << a << " b:" << b << endl;

    system("pause");

    return 0;
}

总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单。

2.3 引用做函数返回值

注意:不要返回局部变量引用,因为局部变量引用会在函数结束调用后进行销毁。

用法:函数调用作为左值

2.4 引用的本质

//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
    ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
    int a = 10;
    
    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
    int& ref = a; 
    ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
    
    cout << "a:" << a << endl;
    cout << "ref:" << ref << endl;
    
    func(a);
    return 0;
}

引用的本质在c++内部实现是一个指针常量.

2.5 常量引用

作用:常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参,例如:const int& ref = 10;

3. 函数声明

3.1 函数默认参数

C++中,函数的形参列表中的形参可以拥有默认值。

语法:返回值类型 函数名 (参数= 默认值){}

int func(int a, int b = 10, int c = 10) {
    return a + b + c;
}

//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
    return a + b;
}

int main() {

    cout << "ret = " << func(20, 20) << endl;
    cout << "ret = " << func(100) << endl;

    system("pause");

    return 0;
}

3.2 占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型){}

主要用在类的多态。

//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
    cout << "this is func" << endl;
}

int main() {

    func(10,10); //占位参数必须填补

    system("pause");

    return 0;
}

3.3 函数的重载

作用:函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同 或者 个数不同 或者 顺序不同
//函数重载需要函数都在同一个作用域下
void func()
{
    cout << "func 的调用!" << endl;
}
void func(int a)
{
    cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
    cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
    cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a ,int b)
{
    cout << "func (double a ,int b)的调用!" << endl;
}

//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
//  cout << "func (double a ,int b)的调用!" << endl;
//}


int main() {

    func();
    func(10);
    func(3.14);
    func(10,3.14);
    func(3.14 , 10);
    
    system("pause");

    return 0;
}

3.4 注意

尽量不要出现二义性:

void func2(int a, int b = 10)
{
    cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
    cout << "func2(int a) 调用" << endl;
}

上面这个函数重载就具有二义性,编译器不知道应该调用哪个函数。

4. 类和对象

面向对象的三大特性:

  1. 封装
  2. 继承
  3. 多态

4.1 访问权限

  1. public 公共权限 类内可以访问 类外可以访问
  2. protected 保护权限 类内可以访问 类外不可以访问
  3. private 私有权限 类内可以访问 类外不可以访问

4.2 struct和class的区别

在C++中 struct和class唯一的区别就在于 默认的访问权限不同

区别:

  • struct 默认权限为公共
  • class 默认权限为私有

4.3 构造函数和析构函数

在其它高级编程语言中,构造函数经常使用,析构函数很少会用到,因为其它高级编程语言中自带垃圾回收机制(GC),而C++中则没有自动垃圾回收机制,所以需要通过析构函数进行销毁对象、变量。

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

4.3.1 析构函数

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

4.3.2 构造函数

在C++中自带普通构造函数和拷贝构造函数。

拷贝构造函数调用:

C++中拷贝构造函数调用时机通常有三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

4.3.3 总结

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其它构造函数

4.4 深拷贝和浅拷贝(重点)

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

4.5 初始化列表

class Person {
public:

    ////传统方式初始化
    //Person(int a, int b, int c) {
    //  m_A = a;
    //  m_B = b;
    //  m_C = c;
    //}

    //初始化列表方式初始化
    Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
    void PrintPerson() {
        cout << "mA:" << m_A << endl;
        cout << "mB:" << m_B << endl;
        cout << "mC:" << m_C << endl;
    }
private:
    int m_A;
    int m_B;
    int m_C;
};

int main() {

    Person p(1, 2, 3);
    p.PrintPerson();


    system("pause");

    return 0;
}

语法:构造函数():属性1(值1),属性2(值2)... {}

4.6 常函数和常对象

常函数:

  • 成员函数后加const后我们称为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
class Person {
public:
    Person() {
        m_A = 0;
        m_B = 0;
    }

    //this指针的本质是一个指针常量,指针的指向不可修改
    //如果想让指针指向的值也不可以修改,需要声明常函数
    void ShowPerson() const {
        //const Type* const pointer;
        //this = NULL; //不能修改指针的指向 Person* const this;
        //this->mA = 100; //但是this指针指向的对象的数据是可以修改的

        //const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
        this->m_B = 100;
    }

    void MyFunc() const {
        //mA = 10000;
    }

public:
    int m_A;
    mutable int m_B; //可修改 可变的
};


//const修饰对象  常对象
void test01() {

    const Person person; //常量对象  
    cout << person.m_A << endl;
    //person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
    person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

    //常对象访问成员函数
    person.MyFunc(); //常对象不能调用const的函数

}

int main() {

    test01();

    system("pause");

    return 0;
}

4.7 友元

私有属性(Private) 也想让类外特殊的一些函数或者类进行访问,就需要用到友元(friend)

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

4.8 运算符重载

函数名 operator运算符()

例如:Person operator+(const Person& p)

几乎所有的运算符都能进行重载。

4.9 多继承(慎用)

其它编程语言中一般为单继承,但是在C++中允许多继承。

语法:class 子类 :继承方式 父类1 , 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

4.10 菱形继承(了解)

两个派生类继承同一个基类

又有某个类同时继承者两个派生类

这种继承被称为菱形继承,或者钻石继承

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

4.11 多态

多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数(函数前面加上virtual关键字)实现运行时多态

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

多态满足条件

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

4.11.1 虚函数和抽象类(接口)(重要)

C++中没有接口的概念,但是可以用抽象类来实现接口。

虚函数

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

4.11.2 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;
类名::~类名(){}

5. 参考资料

视频:https://www.bilibili.com/video/BV1et411b73Z

笔记:https://github.com/AnkerLeng/Cpp-0-1-Resource