C++复习

本文详细回顾了C++中的核心概念,包括函数参数传递、内联函数、默认形参值、函数重载、类型安全、类与对象的访问控制、构造函数与析构函数、静态成员与静态成员函数、友元、常对象与常成员函数、数组、指针、字符串、继承与派生、多态、模板等关键知识点。通过深入理解这些内容,有助于提升C++编程技能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 概述

C语言有的、常用的知识点就不再重复了,下面复习一下C++中一些基本且重要的东西

C++的三大特征: 封装、继承、多态

2 函数补充

2.1 函数参数传递

  • 值传递:不改变实参,因为在调用时会在栈中分配形参空间,并使用实参初始化形参,然后形参就和实参没有关系了。代码我也就不写了,经典swap(a, b);

  • 地址传递:改变实参,传递的是地址,通过解引用访问实参。地址本身还是值传递,但会通过地址去寻找实参并修改实参。

  • 引用传递:改变实参,引用就是实参的一个别名,实质是对变量的间接寻址。形参就是实参,声明引用一定要初始化(error),之后不能再改变。在参数传递过程中会给形参分配空间,并用实参初始化它。

2.2 内联函数inline

内联函数的出现是为了减少一些功能简单、规模较小、频繁调用的函数调用开销

内联函数是用于声明的关键字,他只是一种要求,具体是不是内联函数还得编译器说了算。

2.3 带默认形参值的函数

由于参数传递是自右向左的, 因此参数必须自右向左连续定义, 若该参数没有默认参数值, 那么右边参数都不能有参数值

如果在函数声明中定义过默认参数值, 则不能在函数定义中再定义默认参数值, 即使相同也不行

2.4 函数重载

函数名相同 && (返回值不同 || 形参类型不同 || 形参数量不同 || 是否有const)

2.5 函数声明与类型安全

在编译时, 编译器会检查函数声明与函数调用的正确性, 类型会转换, 若不正确会报错

C++比C语言安全, C语言允许对函数不完整声明
double add();不是说没有参数, 没有参数要写void, 表示参数未知

3 类与对象

简单的我就快速过了,很多都记得就不一一写了,捡重要不会的写

3.1 类成员的访问控制

访问上public > protected = private 后面再写一下继承那的不同

3.2 类成员函数的内联

隐式声明直接把函数定义放在类中就可以了

```C++ 显式声明
class A
{
    pubilc:
        int add(int a, int b);
}
inline int A::add(int a, int b)
{
    return a + b;
}
```

3.3 构造函数与析构函数

3.3.1 构造函数

创建类对象时自动调用构造函数,编译器会自动生成默认构造函数,什么也不干,如果自己写了构造函数,编译器就不会自动生成了,最好自己再写一个默认构造函数。

3.3.2 复制构造函数

A(const A& a)形参是对象的引用, 为什么是引用呢?我觉得有两点:第一点最重要,因为如果不是引用,在参数传递过程中还要调用复制构造函数,就死循环了;而就是加快速度吧

自己没写的话,编译器还会给你生成一个默认的复制构造函数,把你类中所有的数据都复制到里面,注意 ! ! 这是浅拷贝

浅拷贝与深拷贝

复制构造函数的调用时机:

  • 用一个类的对象初始化另一个类的对象时;

  • 函数形参是类的对象,调用函数时,进行形参和实参的拷贝时;

  • 函数的返回值是类的对象时;(这个要好好说一下:被调函数中的类对象属于局部变量,在函数结束时就会被销毁,这样怎么传给主调函数呢?这时编译器就先生成一个临时对象(生存期只在函数调用处的那个表达式b = A();,在执行return a;时会调用复制构造函数到临时的无名对象中,函数结束局部变量a销毁,临时无名对象再赋值给b(此时可不是调用复制构造函数哦,这时候调用的是operator=())!

3.3.3 析构函数

在对象销毁时,会自动调用析构函数,~A();没有返回值,没有参数,但可以是虚函数

3.4 类的组合

类的组合就是描述了一个包含与被包含的关系,也就是have a的关系,将一个类对象封装在另一个类中。

3.4.1 组合类

```C++
class A
{
    public:
        B b;
        C c;
        A(B bb, C cc):b(bb), c(cc)
        {

        }
    ...
}
```

当创建A时,首先要创建B,那么如果A中有多个对象,构造函数的调用顺序是啥样的呢?

  • 首先调用内嵌的对象的构造函数,顺序是按照类内的定义顺序,与构造函数的初始化列表顺序无关

  • 其次调用本类的构造函数

复制构造函数也是这样的

析构函数调用顺序与构造函数完全相反

3.4.2 前向引用声明

就是说如果内嵌这个类对象还没有定义怎么办?可以现在前面写一个class B;,不涉及任何定义,仅仅声明

不能相互内嵌类对象

3.5 结构体和联合体

结构体和类差不多,唯一区别在于访问控制,类内默认私有访问,结构体默认公有访问。

C语言中的struct只能有数据成员,不能有函数成员,也没有访问控制的概念

联合体其他的和结构体差不多

但是, 联合体的全部数据成员共用一组内存空间,同时最多只有一个有意义

4 数据的共享与保护

作用域、可见性、生存期、命名空间不再展开

4.1 类的静态成员

对象和对象之间也需要共享数据

4.1.1 类的静态数据成员

类中的非静态数据成员都是在每个对象中拥有一个复本,而静态数据成员则是在每个类中拥有一个副本,每个对象都可以修改访问。由于静态数据成员不属于任何对象,访问时可以通过类名::标识符来访问,如A::count。

必须在类内声明, 类外定义

```C++
class A
{
    private:
    static int count;
}
int A::count = 0;//专门分配空间,主函数中不允许直接访问
```

4.1.2 类的静态成员函数

那么在为创建任何对象时,如何访问静态成员count呢?那就是静态成员函数,静态成员函数也是属于整个类,可以由类中的所有对象访问。调用静态成员函数可以通过类名或对象名,但是我们一般还是选择类名。

可以直接访问静态成员,但是访问非静态成员必须通过特定的一个对象,因为非静态成员属于每一个对象。

4.2 友元

友元是非成员函数访问类内的私有成员的一种方法,是对数据隐藏和封装的小小破坏。

4.2.1 友元函数

友元函数是在类内用friend修饰的非成员函数

```C++
class Point
{
    private:
        int x, y;
    public:
        friend int dist(Point& xx, Point& yy)const;
}
int dist(Point& xx, Point& yy)const
{
    int x = xx.x - yy.x;//可以直接访问Point的私有成员x、y
    int y = xx.y - yy.y;
    return ...
}
```

4.2.2 友元类

若A类是B类的友元类,则说明A的所有成员函数都是B的友元成员函数,可以访问B中的私有成员啦 friedn class A;

  • 友元不可传递

  • 友元单向

  • 友元不可继承

4.3 常对象

4.3.1 常对象

常对象的数据成员在整个生命期内不能被修改,必须初始化,不能改变。

那常对象的成员函数呢?通过成员函数好像也能修改数据成员欸,这可不行!所以C++规定常对象不能调用普通成员函数,那好像没啥用啊,这个东西。所以又引入了常成员函数,常对象可以调用常成员函数,是常对象的唯一对外接口。

4.3.2 常成员函数

就是在最后加一个const来,可以用来区分重载函数。

不管你是常对象还是普通对象,调用常成员函数在这个生命期中你就变成了常对象,不能修改,不能调用非const修饰的成员函数

4.3.3 常数据成员

不能修改,必须初始化,只能通过构造函数的初始化列表初始化

static const int count = 0;//具有整数或枚举类型的静态常量成员可以不用在类外定义,直接在类内定义也可以

4.3.2 常引用

Point(const Point& p1)常引用的对象也不能被更新,一个常引用,不管你绑定到什么对象上(常、非 常对象)你都不能再被更新了。非 常引用 不能绑定到常对象上。

在函数中不改变值使用引用时,最好使用常引用,防止常对象无法传入

5 数组、指针、字符串

数组就不说了,就一个数组名是数组的首地址。

指针一些基本的也先不写了,写一下不太清晰的吧!

5.1 指针

5.1.1 函数指针与指针函数

指针函数:就是返回值是指针的函数,通过返回指针可以返回大量数据的地址

函数指针:指向函数的指针

函数名是函数的首地址

int (*pfun)(int, int);//声明函数指针

typedef在函数指针这也不太一样,是这样的

typedef int (*otherName)(int, int);//other就是这个函数指针类型的新别名

otherName pfun;

函数指针初始化otherName pfun = fun//(函数名);

5.1.2 对象指针

对象指针占用的内存空间只是用来存放数据的,函数不保存每个副本

this指针

this指针是类非静态成员函数的一个隐含参数,在调用成员函数时,目的对象的地址就会赋值给this指针,用于访问对象内成员。

this指针是一个常量指针

5.2 动态分配 new/delete

在堆上动态分配对象,自己申请了自己要释放。

```C++
int* p = new int(2);//初值为2
int* p1 = new int;//未初始化
int* p2 = new int();//对于基本数据类型会初始化为0
delete p;//调用析构函数
delete p1;
delete p2;
int* p = new int [2];//加() == new T(); 不加() == new T()
delete [];
```

new一个对象时,如果类中存在默认构造函数,则new T和new T()相同;

若没有用户写,调用new T会生成一个默认构造并调用,调用new T()就会为里面的基本类型初始化为0之类的。

delete只能释放一次,不能多次对同一内存进行释放

内存泄漏 memory leak

5.3 string类

```C++
string();
string(const string& s);
string(const char* s);
```

操作有 + / += / == / !=

substr()、replace()、find()等等

5.4 !指针与引用的区别

6 继承与派生

6.1 继承与派生

类的继承是新的类从原有类中获取已有的特性,从已有类产生新的类叫做派生,是一个is a的关系

基类、父类 派生类、子类 直接基类 间接基类

```C++
class Derived : public Base1, public Base2
{
    public:
        Derived();
        ~Derived();
}; 
```
  • 多继承:一个派生类有多个基类
  • 单继承:一个派生类只有一个基类
  • 多重派生:一个基类派生出多个派生类

继承方式决定的是派生类如何访问从基类继承的成员

默认private

派生类成员:除了从基类继承(吸收)来的成员(没有构造和析构),新增加的成员

6.2 访问控制

保护成员扩大的访问范围表现在:基类的保护成员可以在派生类的成员函数中被访问。

我们主要说派生类如何访问从基类继承来的那些成员(取决于继承方式)

  • public继承:基类中的public protected在派生类中保持不变 基类中的public派生类和类外成员都可以访问 protected类内可以访问,类外不可以 private成员不可直接访问(派生类的成员也不可访问,只有基类的公有成员才能访问)
  • private继承:基类中public protected的变成私有成员 private成员不可直接访问
  • protected继承:基类中public和protected变成保护成员,private成员不可访问 保护是指类内是public,类外是private

6.3 类型兼容规则

就是说公有派生类对象可以代替基类对象(但只能用基类中的成员),基类对象有的基本他都有,但是替换后的派生类对象仅具有基类对象的作用。

  • 派生类的对象可以隐含转换为基类对象
  • 派生类的对象可以初始化为基类的引用
  • 派生类的指针可以隐含转换为基类的指针

6.4 派生类的构造、复制构造、析构

派生类不继承基类的构造、析构函数,派生类的构造函数只能初始化派生类成员,对于基类成员要通过基类构造函数构造

6.4.1 构造函数

构造派生类先构造基类

对派生类初始化,若基类有带形参的构造函数,那么派生类必须也要写一个构造函数,提供一个向基类传递参数的途径

派生类构造函数的执行次序

  • 基类构造函数,按照继承时声明顺序

  • 新增成员对象初始化,按照类内的定义顺序(与初始化列表无关)

  • 派生类构造函数中的内容

    
    class D: public B2, public B1, public B3
    {
        public:
            D(int a, int b, int c, int d):B1(a), member2(d), member1(c), B2(b){}
        private:
            B1 member1;
            B2 member2;
            B3 member3;
    }
    
    // B2(b) B1(a) B3(*) m1(c) m2(d) m3(*) 
    

6.4.2 复制构造函数

```C++
Derived::Derived(const Derived& v):Base(v){}//v是Derived隐式类型兼容到Base
```

6.4.3 析构函数

派生类的析构函数需要对新加入的非对象成员清理,对于基类和对象成员,系统会自动调用析构函数

调用顺序正好和构造函数完全相反

  • 先析构函数体内
  • 再析构派生类成员
  • 析构基类

6.5 派生类的访问

作用域分为外层和内层, 内层对外层隐藏,派生类在内层,基类在外层
重载 重写 隐藏

如果派生类中声明了与基类同名的新函数,即使参数表不同,从基类继承的同名函数的所有重载形式都会被隐藏,要访问隐藏的成员,需要通过作用域分辨符::和基类名来限定

  • 函数隐藏:派生类中定义的与基类中同名的函数(参数表不同也行)
  • 函数重载overload:在统一作用域下同名参数表不同的函数
  • 函数重写override(多态):就是派生类要改写基类的函数,需要声明和基类完全相同+函数定义不同+virtual限定(见多态)

for example:

```C++
class Base1
{
    private:
        int var1;
    public:
        Base1(int n):var1(n){}
        Base1(){}
        ~Base1(){}
        void update(int n)
        {
            var1 = n;
            cout << "Base1" << endl;
        }
        void show()
        {
            cout << var1 << endl;
        }
};

class Derived : public Base1
{
    private:
        int var1;//同名数据成员
    public:
        Derived(int n1, int n2):Base1(n1), var1(n2){}
        ~Derived(){}
        void update(double n)
        {
            var1 = n;//只能看见新的,Base1中的被隐藏了
            cout << "Derived" << endl;
        }
        void print()
        {
            cout << var1 << endl;
        }
};
int main()
{
    Derived d(1, 2);//新的是2,旧的是1
    d.update(8);
    d.show();
    d.print();// output: Derived 1 8(调用的是派生类中的update,即使参数是double,改写的也是派生类中的var1 基类中的update和var1都被隐藏了)


    /*d.Base1::update(8);
    d.show();
    d.print();*/ //output:Base1 8 2(调用的是基类中的update,改写的是基类中的var1
    return 0;
}
```

using Base1:update;就可以把Base1中的update引入到派生类的作用域中,这样就有两个update,参数不同,属于函数重载

update(8)就是调用基类, update(8.6)就是调用派生类

Base0 -> Base1 -> Derived

Base0 -> Base2 -> Derived

这样Derived中就有一个从Base1获得的Base0的东西和一个从Base2获得的Base0的东西,无法通过Base0区分,需要通过直接基类Base1和Base2限定

其实只有一份Base0中的代码,但是有两个Base0的子对象,调用非静态函数需要指定对象来初始化this指针,所以需要指定哪一个对象啊

我们也可以通过虚基类来解决上述问题

6.6 虚基类

用于解决多重继承同名二义性问题

将上述的共同基类设置为虚基类,这样就只有一个同名数据成员和同一个函数成员了

虚基类的声明是在派生类的继承中实现的

class Base1:virtual public Base0{};

最远派生类(Derived)调用基类构造函数初始化

Derivied(int var):Base0(var), Base1(var), Base2(var){}//只有Base0()会调用Base0的构造函数,Base1 2()会忽略 也就是说Base0的构造函数只会调用1遍,不会再通过Base1 2调用Base0的构造函数啦

base0 -> Base1 -> Base2 -> Derivied

调用顺序:

  • 先执行虚基类的构造函数
  • 其他基类按照继承顺序
  • 类中成员按照类中定义顺序
  • 派生类函数体

7 多态

7.1 概述

多态的分类:重载多态(函数重载、运算符重载)、强制多态(类型转换)、包含多态(虚函数)、参数多态(模板)

运行时多态(3)、编译时多态(124) 绑定

7.2 运算符重载

基本上都能重载(除了. * :: ?😃 不能重载没有的 优先级、结合性、操作数、功能不能变

7.2.1 成员函数运算符重载

操作数少一个

oprd B => oprd.B()

oprd1 B oprd2 => oprd1.operator B(oprd2)

后置++ – 形参为int ,区分前置后置

```C++
Clock& Clock::operator++()//前置++
{
    second++;
    ....
    return *this;//这个this非临时变量
}
Clock Clock::operator++(int)//后置++
{
    Clock old = *this;
    ++(*this);//前置++
    return old;//返回临时变量不能用引用
    //如果返回值是&类型的话,编译器就不会给你在调用处生成一个临时无名变量了,这时候局部变量销毁,就无法传给主调函数了
}
```

7.2.1 非成员函数运算符重载

oprd1 B oprd2 => operator B(oprd1, oprd2)

operator++(oprd, int) operator++(oprd)

```C++ 
ostream&  operator<<(ostream& out, const Complex&c) //cout << complex;
{
    out << "  ";
    return out;
}//只能重载为非函数成员,因为第一个操作数是ostream,返回值是ostream是因为可能会有连续输出 << <<
```

若要访问类的私有成员需要设为类的友元

7.3 虚函数

改写继承成员函数

虚函数这一部分,我记得我大一的时候学的挺清楚的了,当时还有一个学长也就是我现在这个阶段,问我虚函数的问题,我都能对答如流,现在两年了,回过头来看,突然发现虚函数这个东西到底是为什么存在呢?

问题提出:派生类继承基类中的部分,我们想要改写基类中的函数,那我们就可以写一个和基类同名的函数,但是这样会覆盖(这个覆盖的意思我觉得是在当前派生类作用域下基类函数不可见,并不是说没有了)基类的函数,那我们又想保留父类中的函数怎么办呢?我们采用类型兼容的方法,就是说用派生类的指针(引用)赋值给基类的指针(引用),这样用这个指针就可以访问到基类的指针了,但是这个指针又只能访问基类的部分,真是麻烦。

其实我们就是又想要基类继承的函数,又想要派生类改写的函数,如果没有虚函数我们也可以做到的

```C++
Base1 b1;
Base2 b2;
Derivied d;
d.display(); //子类改写的函数
Base1* pb1 = new Derivied;
pb1->display();//类型兼容调用的基类函数
delete pb1;
```

能做到是能做到,但是这是两种方法可能是不统一吧,一个直接用对象调用,一个得用基类指针调用。
所以引入了虚函数的概念,使用派生类指针就调用的是改写的,使用基类指针就用的是基类函数。

```C++

#include<iostream>
using namespace std;

class Base1
{
public:
    virtual void display()const;
};
void Base1::display()const{
    cout << "Base1" << endl;
}

class Base2:public Base1
{
public:
    void display()const;
};

void Base2::display()const{
    cout << "Base2" << endl;
}


class Derivied:public Base2
{
public:
    void display()const;
};
void Derivied::display()const{
    cout << "Derivied" << endl;
}

void fun(Base1* ptr)
{
    ptr->display();
}
int main()
{
    Base1 b1;
    Base2 b2;
    Derivied d;
    d.display();
    fun(&b1);
    fun(&b2);
    fun(&d);
    return 0;
}
```

上述代码可以看到如果是虚函数的话,就会根据调用对象指针的类型判断调用哪个函数,是在运行中查询不同对象的虚函数表得到的函数入口地址,但是如果你去掉virtual,不管使用哪一个都会转化成基类指针,都是调用的Base0中的,也就是在编译的时候都已经确定了的。

“所谓多态就是同一段代码调用一个对象的某个固定的方法,可以根据对象类型不同调用不同的实现,虚函数的作用是这个,让你在无需了解具体子类类型的情况下,通过基类指针调用子类的实现。”

可以看到派生类中没有给出virtual,但是系统会自己判断是不是虚函数(和基类虚函数是否有相同的声明)

虚析构函数:

某个基类是虚析构函数那么子类也都是虚析构函数

基类指针调用析构函数不会调用派生类的析构函数,会导致Memory Leak, 特别是当派生类中含有指针变量的时候

7.4 纯虚函数和抽象类

纯虚函数是一个在基类中声明的虚函数,基类中不用定义(也可以写,必须被派生类覆盖,否则无法实例化),要求派生类对其进行定义

virtual void sum(int, int) = 0;

纯虚函数在抽象类里,抽象类无法实例化,纯虚函数没有函数体,函数体要由派生类实现

带有纯虚函数的类叫做抽象类,简历一个公共接口

虽然抽象类不能实例化,但可以定义指针、引用,通过指针、引用访问派生类对象

8 模板

模板的语法我就不写了

8.1 函数模板

当类型参数确定后,编译器将以函数模板为样板,生成一个函数,这一过程叫做函数模板的实例化

函数模板在编译时不会生成任何目标代码(函数模板不是函数),只有由模板生成的实例才会生成目标代码

8.2 类模板

类模板(类家族)实例化生成类,类实例化生成对象

类模板不是一个有效的数据类型,只有类模板实例化的类才是一个数据类型

不同参数实例化的类也是不同的数据类型 A != A

8.3 模板的特化与偏特化

特化:全部

偏特化:范围和个数

C++不允许将函数模板偏特化,但函数模板可以重载

```C++
template<typename T>
T Max(T a, T b)
{
    return a > b ? a : b; 
}
//函数模板重载
template<typename T>
T* Max(T* a, T* b)
{
    return *a > *b ? *a : *b; 
}
```

9 GP && STL

10 流类库与输入输出

I/O流类库是C语言中I/O函数的替换产品

I/O流类库之间的关系

cin >> cin.get() getline()

11 异常处理

throw expression;

try{

}
catch(){

}
catch()
{

}
虚函数表

本来以为一个晚上就能弄完,真是小看了,弄了快一整天了,还有很多坑要填,先就这样 ! bye~

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值