C++面向对象编程笔记(万字笔记,纯知识点,用于复习)

在C++的编程中,面向对象编程(OOP)和运算符重载是两项重要的技术。面向对象编程允许通过封装、继承、多态等概念来构建更加模块化和可维护的代码,而运算符重载则提供了扩展C++内置运算符功能的能力。作为一位编程小白,我在学习和实践这些概念的过程中写了一些笔记。

笔记中没有示例代码,纯知识点,不适合学习使用,倒是适合学过的人做一个快速的回顾(扫描知识点)和复习。

对于文章中有错误的内容,欢迎批评和指正。

如有侵权,请联系删除。

目录

前言

正文

一、类与对象

1.基本介绍

2.构造函数与析构函数

3.类的其他成员

4.访问权限

二、运算符重载

三、继承

1.继承中的权限问题

2.基类和派生类的对象赋值。

3.继承中的作用域

4.继承中的构造函数

5.继承与友元

6.多继承

7.继承中的其他问题

四、虚函数与多态

1.引入

2.虚函数与重写

3 override和final

4纯虚函数与抽象类

5虚表

五、模板

1.引入

2.函数模板与重载

3.函数模板特化

4.类模板

5.模板与继承

6.类模板中成员函数的实现

7.类模板与友元

六、输入/输出流

1. 流类和流对象

2.标准流和流操作

3.串流

4.文件处理

七,异常捕获

1.try-catch

2.throw

3.带异常说明的函数原型

4.异常捕获的其他应用

总结


前言

面向对象编程的核心概念包括封装、继承和多态。封装通过将数据和操作数据的函数组合成一个整体(即对象),实现了对数据的隐藏和保护。继承允许我们创建一个新的类(子类或派生类),继承一个已存在的类(父类或基类)的成员变量和成员函数,从而实现代码的重用和组织。多态则允许我们使用父类类型的引用或指针来调用子类的成员函数,实现了代码的灵活性和可扩展性。

运算符重载是C++的一个独特特性,它允许我们重新定义或重载大部分内置运算符,以便它们可以用于用户自定义的数据类型。通过运算符重载,我们可以使自定义类型的使用更加自然和直观,提高代码的可读性和可维护性。

模板是C++中的一项强大功能,它允许我们编写与类型无关的代码。通过模板,我们可以定义一种通用的数据结构或算法,并在编译时根据具体类型生成相应的代码。这种机制使得我们可以更加灵活地处理不同类型的数据,提高代码的复用性和可维护性。在C++中,我们可以使用函数模板和类模板来定义通用的数据结构或算法。STL是C++标准库中的一部分,它提供了一组高效、通用的容器和算法。这些容器和算法基于模板实现,因此具有高度的灵活性和可重用性。


正文

一、类与对象

1.基本介绍

1.1 成员属性的访问和成员函数的调用。

1.2 this指针的使用,它是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

2.构造函数与析构函数

2.1 构造函数:创建对象时自动调用的函数。析构函数:对象没了时自动调用的函数。

在同一个代码块中,先构造的对象后析构。

2.2 构造函数有无参构造、有参构造和拷贝构造(可以通俗地认为它们的等级由低到高),编译器为每个类自动都设置好了这几个函数,重写了一个较高级的函数时,较低级的函数就都没了(意味着你得重新重写它们)

2.3 深浅拷贝。编译器默认实现的是浅拷贝。直接将所有属性照搬是浅拷贝。浅拷贝的缺点在于,如果某个属性是指针,那么两个对象的指针会指向同一处,在调用析构函数时会被析构两次。深拷贝不是简简单单的把整个值拿过来,而是在堆区开一个新的。

2.4 用一个类对象初始化另一个类对象时,用的是拷贝构造。用一个类对象给另一个类对象赋值时,用的是重载=。

3.类的其他成员

3.1 类的常成员:常成员属性(说白了就是这个属性不能被修改,如果整个对象都是常的,那么所有属性都不能被修改),常成员函数(不能修改成员属性)

3.2 静态属性。用static关键词,就如同函数的静态变量是所有该函数的调用共享一样,类的静态属性是该类共享(包括该类本身以及该类创建的所有对象)

3.3 静态成员函数。是一个公属于类的函数。是公属于类的函数,所以只能调用公属于类的变量(静态属性),不能调用成员属性(因为那是某一个对象特有的)。

静态属性和静态成员函数都是公属于类的,既可以由类直接调用(加类::),也可以由对象调用。

3.4友元。分为友元函数和友元类。友元类的所有成员函数为友元函数。友元可以访问到非公有属性。

4.访问权限

4.1 public:可以在类的内部和外部访问。

protected:只能在类的内部以及子类中访问。

private:只能在类的内部访问。可以使用公有成员函数,用于获取私有变量的值。

4.2 默认情况下,类的成员(包括属性和方法)的访问权限是private。

4.3 私有成员不能直接通过类的实例化对象访问。但是可以通过友元访问,或者通过内部的公有属性函数访问。

二、运算符重载

重载有两种实现方式,一种是用成员函数重载,另一种是用友元函数重载。

关联和区别:

1.成员函数本质上是类内的,有this指针。本质上是由对象打点儿调用。

友元函数,本质上是类外的,没有this指针。本质上是一个函数,然后将对象作为一个参数传进去。

2.参与运算符的元素个数叫操作数(如加法左右各一个元素,所以操作数是2)。成员函数,本质上是对象在调用这个函数,函数中已经有一个this了,所以需要传入的参数的数量等于操作数减一。友元函数,没有this指针,所以参数的数量等于操作数。

3.用成员函数进行运算符重载时,左操作数和右操作数的地位是不对等的,左操作数是this所指的对象,右操作数后来传进来的参数。友元函数的左操作数和右操作数的地位是一样的,同时,友元函数更加容易发生隐式转换(当左操作数为对象,右操作数不是对象时,成员函数的重载可以发生隐式转换,当然前提是提供了隐式转换所需的构造函数。当左操作数不是对象时,成员函数的重载不能发生隐式转换,而友元函数的重载可以)。

4.成员函数有this,所以可以直接对对象的值进行修改。当使用友元函数重载时要对对象的值进行修改时,记得传入对象的引用。

5.必须用成员函数重载的:=,(),[],->。必须使用友元函数的:<<和>>。

常见的重载练习:

1.重载+(也可以是双目运算符)

2.重载-(也可以是其他单目运算符)

3.重载=(也可以是+=之类)

4.重载==(也可以是<等)

5.重载<<和>>

6.重载前置++和后置++

7.重载[]

8.重载() (重载小括号也叫做仿函数,仿函数参数不同也能实现重载,这里是函数重载,不是运算符重载)

9.类型转换函数

三、继承

1.继承中的权限问题

1.1 在任何继承方式中,父类中的private属性,子类拿不到。(注意,继承到了,因为父类所有成员都要被继承。但是拿不到)

1.2 public继承中,父类的public属性在子类中仍然是public,父类的protected的属性在子类中仍然是protected。

protected继承中,子类继承到的属性都是protected。

private继承中子类继承到的属性都是private。

(其实就是,子类的权限=父类的权限和继承方式中更严格的那个)

1.3 class中,没有给出继承方式,默认是private继承。(在多继承中,每一次继承都要写继承方式,否则就会被认为是private)

Struct中,没有给出继承方式,默认是public继承。

1.4 class和struct的区别。

前者属性默认private,后者属性默认public

前者继承默认Private,或者继承默认public

前者能定义模板,后者不行

2.基类和派生类的对象赋值。

2.1子类对象可以赋值给父类的对象,指针或者引用。

反之不行。

子类赋值给父类时,有多的,没问题,顶多是多的那部分拿不到了。无伤大雅。

父类赋值给子类时,有少,所以不行。如果实在要这么做,可以进行类型的强行转换。

2.2 子类对象赋值给父类的对象,指针或者引用,有个形象的说法叫做切片。因为如果这样做,以父类指针指向子类对象为例,这个指针只能拿到子类中继承自父类的东西,不能拿到子类中新添的东西。

3.继承中的作用域

3.1 父类和子类都有独立的作用域。也就是说,当两者出现同名的函数时,并不构成重载(函数重载要求在同一个作用域中)

3.2 属性不重名时,子类可以任性地调用子类或者父类属性和函数(父类private除外)

属性同名时,只会调用子类的。如果想调用父类的,那就加个父类的作用域。

3.3 重写:属性或函数同名时,只会调用子类的,不会调用父类的。这样就形成了对父类成员函数的重写(或者是对父类成员函数的隐藏)。需要注意的是,只要函数名称相同就构成隐藏(与函数的返回值类型以及参数列表都无关,也就是说即使返回值类型不一样,只要函数名相同就构成重写)

4.继承中的构造函数

4.1 子类对象在构造时分成了两步,第一步是用父类的构造函数来初始化从父类继承下来的那一部分成员(父类的变量成员不可以在子类的初始化列表中进行初始化,因为它不属于子类),第二部是初始化自己新增的成员。

4.2 如果父类中没有定义构造函数,或者定义了无参的构造函数或者全缺省的构造函数。那么子类的构造函数中可以不用调用父类的构造函数。

如果父类中显示的定义了有参数的构造函数,那么子类的构造函数初始化列表中就要调用父类的构造函数。(这里有种情况要注意一下。情况一:当子类继承父类时,构造函数初始化列表中会调用父类的构造函数。情况二:a类的属性中有b类的对象时,在a类的构造函数初始化列表中也会对b类的对象进行初始化。这两种情况长得很像)

4.3 对于拷贝构造或重载=号,都应该先调用父类的拷贝构造或重载=号,然后再对子类新增的部分进行处理。

这里有一个处理技巧。要把子类对象1拷贝给子类对象2。调用父类的拷贝构造时,可以直接把2放进父类的拷贝构造中。因为父类的拷贝构造只能“管的到”父类有的部分(即切片),这样就直接把父类的部分拷贝完了。之后单独处理子类新增部分即可。

4.4 析构函数。由于析构函数是没有参数的,所以情况是和“没有参数的构造函数”是一样的。子类的析构函数会在调用后自动调用父类的析构函数。所以就不要我们单独费心了。

4.5 构造函数和析构函数的调用顺序问题:先父类构造再子类构造,先子类析构再父类析构。

5.继承与友元

5.1 友元函数和友元类这种“朋友关系”,不能被子类继承。

5.2 友元函数可以访问父类中的成员变量,可以访问子类中继承自父类的成员变量,但不可以访问子类中单独增加的成员变量。

6.多继承

6.1 多继承就是一个子类有多个父类。在多继承时,每个父类前面都要加权限,否则就会是默认的private。

6.2 菱形继承中会出现二义性问题。我们可以加作用域让访问明确化,但是这样并没有从根本上解决问题。

6.3 可以采用虚继承。在继承前加一个关键字virtual,使得被重复继承的东西就只留独一份。

7.继承中的其他问题

7.1 带final关键词说明的类不能被继承。

7.2 父类的静态成员变量能够被继承,而且所有子类以及父类中的静态成员变量是同一份。也就是说,这个静态变量是所有这些类以及所有这些类的对象共有的。

静态成员函数同理。

四、虚函数与多态

1.引入

1.1 多态,具体定义我说不出来呜呜呜

1.2 多态的分类。分为静态多态和动态多态。

静态多态:函数在编译期间就已经确定了。比如函数重载,模板。

动态多态:程序运行时才能确定。

2.虚函数与重写

2.1 前文已经提到,用父类指针指向子类对象时(也可以是父类引用,然后用子类对象赋值。这里以指针为例),该指针只能调用子类中继承自父类的东西。

当子类重写了父类中的函数时,用该指针调用这个函数,调用的是父类中的(因为只能调用到子类中继承自父类的东西)。

但是我们想的是,用该指针调用这个函数时,要调用子类的。

2.2 虚函数与重写的规则

  1. 必须在继承的体系中
  2. 父类中的成员函数必须是虚函数(用关键词virtual)。子类重写之后既可以是虚函数,也可以不是虚函数
  3. 重写之后,子类的函数与父类的式函数的原型(包括返回值类型,函数名字,参数列表)必须保持一样。
  4. 协变。这是一种特殊情况,重写之后,函数的返回值类型可以不同。但是要求,父类的虚函数必须返回父类对象的指针或者引用,子类的虚函数必须返回子类的对象或者引用。这里的父类对象或者子类对象可以是不同继承体系下的对象(也就是说,父类a的虚函数可以返回父类b对象的指针或者引用,子类a的虚函数也可以返回子类b的对象或者引用,只要保证子类a继承父类a,子类b继承父类b即可)
  5. 有一种情况下,重写函数的名字可以不同。那就是析构函数。
  6. 重写与函数的访问权限没有关系。

2.3 接着2.1继续说,父类指针指向子类对象时,调用子类重写过的函数,只能调用父类的(因为父类指针只能调用子类继承自父类的函数)。

但如果子类重写的函数是虚函数,那么调用的就是子类重写之后的函数。这就是虚函数的特点。

当同一个父类被多个子类继承时,父类中的虚函数可以被不同的子类进行不同的重写,从而父类指针指向不同的子类对象时,可以调用不同版本的重写后的函数。多态就实现啦。

2.4关于虚析构函数的问题。没有使用虚函数时,父类指针指向子类对象时,进行析构时,只能调用父类的析构函数,显然父类的析构函数不能将子类对象完全析构(因为子类还有新添的部分)

将父类的析构函数写成虚析构函数,那么数类指针指向子类对象时,进行析构时,调用的是子类的析构函数(前文提到过,析构函数是一个特例,直接用子类重写父类,不要求函数名字一样),这样就析构干净了。

3 override和final

3.1override。有时候是否重写成功并不容易发现,此时可以在重写之后的函数主体前加上override,此时在编译阶段就会检测该关键字修饰的函数是否重写成功。

显然,这个关键字只能修饰子类的虚函数。

3.2final。有两种用法。

第一种,用来修饰虚函数(不能修饰普通函数)。修饰之后,接下来的子类就不能对其进行重写了。用于在多代继承中,继承到某一代,之后该虚函数就不能被重写了。

第二种,用来修饰类。修饰完一个类之后,这个类就不能被其他类继承了。

4纯虚函数与抽象类

4.1纯虚函数比虚函数更上一层楼。在虚函数的基础上,加一个“=0”。

纯虚函数可以有函数体,但是没意义。

说白了,纯虚函数就是摆明了要被重写的。

4.2包含纯虚函数的类叫做抽象类,也叫做接口类。

抽象类不能实例化出对象(可以理解为本身不完整) 。但是抽象类却可以创建指针和引用。

4.3抽象类一定要被继承。继承之后,子类仍然为抽象类。子类对抽象类内所有的虚函数(当然包括纯虚函数)进行重写之后,子类成为非抽象类。

4.4普通的继承是一种实现继承,继承到了父类的函数和属性。

虚函数的继承,是一种接口继承。并没有拿到实实在在的函数(呃,比如说他碰到了纯虚函数),子类继承的是父类函数的接口(统一一下接口嘿嘿),达成实现多态的目的。

5虚表

5.1有虚函数的类,类中多了一个指针——虚表指针。指向一个虚表,表中记录着虚函数的地址。

5.2继承时,子类将父类中的属性和函数继承后,将父类的虚表指针也继承了,同时又有一个自己的虚表指针。如果子类重写了父类的虚函数,那么重写后的函数地址就会替换表中原来父类的虚函数的入口地址。

那么父类指针指向子类对象时,就会调用子类重写后的那个。

五、模板

1.引入

模板就是把类型也做了一个参数化,建立一个通用的模板,使得同一个功能对各种类型的参数都适用。

2.函数模板与重载

2.1在一个函数前加上template<typename T>,以此来声明这是一个函数模板,其中typename可以被class代替,T是一种虚拟的、未定的类型。

2.2使用函数模板时,既可以直接使用函数模板,也可以在其后面加一个<>然后再将括号里面加上指定的数据类型。当使用后者时,由于函数模板中的未定的类型已经确定了,所以使用起来和普通的函数没啥区别。

2.3如果直接使用函数模板而不指定数据类型,那么编译器会进行自动类型推导,只有推导成功才可以使用。同时,同一个未定类型T只能代表一种数据类型,不能代表多种。

2.4当函数模板中的未定类型确定了之后,就成为了函数模板实例化的一个模板函数,使用起来和普通函数没啥区别。这两个名字念起来有点绕口。

2.5函数模板可以和函数模板或者普通函数重载。

普通函数可以发生隐式类型转换。

显示指定类型的函数模板同普通函数,也可以发生隐式类型转换。

函数模板直接调用,在自动类型推导之后,不会发生隐式类型转换。

2.6发生重载时,优先匹配的顺序为:

类型完全匹配的普通函数。

类型可以自动推导匹配的函数模板。

类型不能完全匹配,但是可以进行隐式转换,然后匹配的普通函数。

也就是普通函数>函数模板>要进行隐式转换的普通函数

函数模板和普通函数重载时,优先调用普通函数。如果想强制调用函数模板,可以加入空模板参数列表<>。

如果调用有多余一个的匹配选择,那么匹配就会出现二义性。

2.7不同的类中如果有同名的函数func,那么写函数模板时,用未定义的类的对象调用func。那么在函数中传入不同类的对象就可以调用不同的func。

3.函数模板特化

3.1假设用函数模板写一个排序问题。可以很轻松的解决各种基本数据类型的排序,但是如果要解决一种类类型的排序,就无能为力了。

3.2此时可以使用模板特化。对特殊的类型进行单独的处理。语法方面,正常写出对于该类型处理的函数,然后在函数前加上template<>。

如此做,当函数模板碰到这种类型时,会自动走那个“特殊处理的函数”。

4.类模板

4.1建立一个类模板的语法和函数模板一样,在前面加template然后再说明一下未定的类型即可。

4.2当类模板中的未定数据类型被指定时,类模板实例化成一个模板类。这名字也是很拗口啊。指定了未定数据类型的类模板,用起来就和普通的类一样了。

4.3类模板没有自动类型推导,所以必须显示的给出未定类的数据类型。

4.4类模板给函数传参的问题。(这一段已经足够抽象了,太难用口头表述了,难蚌)

  1. 可以显示的指出类模板的未定参数,然后将类模板当做一个普通类看待。
  2. 把函数改为函数模板,将函数模板中的未知类型用来指明类中的未知类型。这个时候函数模板中的未知类型就和类中的未知类型统一了。以后传参时,函数模板中的未知类型确定后,类中的未知类型也能确定了。
  3. 把函数改为函数模板,用函数模板的未知类型来代表整个类类型。

5.模板与继承

5.1对于父类是类模板的情况,可以直接显式指明父类未知类型,如此作将父类看成一个普通的类即可。

5.2也可以将子类改为类模板,将子类的一个未知类型用于指明父类的未知类型(和之前那个函数传参很像)

之后,子类创建对象时,子类的未知类型被显示给出,父类的未知类型也就随之确定了。

6.类模板中成员函数的实现

6.1类模板中成员函数如果在类内实现,那么一切正常。

6.2如果在类外实现,一方面,由于函数中可能带有类的未定类型,函数带有未定类型——函数模板。所以要声明模板。另一方面,在函数名前面加类作用域时,不能简单的用类模板,而应该是用函数的未定类型指定类的未定类型之后的类

7.类模板与友元

对于template <typename T> class X

friend void f1();函数f1成为类模板X实例化的每个模板类的友元函数

template <typename T> friend void 2(X<T> & ); 对特定类型(如double)使模板函数f(X<double>&)成为X<double>的友元

friend void A::f3();A类的成员函数f3成为类模板X实例化的每个模板类的友元函数

template <typename T> friend void B<T>::f4( X<T> & ); 对特定类型(如double),使模板类B<double>的成员函数f4(X<double>&)成为模板类X<double>的友元

friend class Y; Y类的每个成员函数成为类模板X实例化的每个模板类的友元函数

template <typename T> friend class Z<T>;对特定类型(如double),使模板类Z<double>所有成员函数成为模板类X<double>的友元

六、STL

1.引入

1.1 STL(Standard Template Library,标准模板库)是C++标准库中的一部分,提供了大量的通用模板类和函数,用于处理各种数据结构和算法。

1.2 STL采用泛型编程的思想,将算法和数据结构分离,使得算法可以作用于不同类型的数据结构。

1.3 STL主要由容器(Containers)、迭代器(Iterators)、算法(Algorithms)和仿函数(Functors)四个部分组成。

  1. 容器:用于存储数据的对象,如vector、list、deque、set、map等。
  2. 迭代器:一种类似于指针的对象,用于访问容器中的元素。STL中的迭代器提供了统一的接口,使得算法可以作用于不同类型的容器。
  3. 算法:对容器中的元素进行操作的函数,如排序、查找、拷贝等。STL中的算法具有通用性,可以作用于不同类型的容器。
  4. 仿函数:也称为函数对象,是一种具有operator()的类或者结构体,用于作为算法的参数,实现自定义的操作。

2.STL容器

2.1 序列容器

stack(栈):一种后进先出(LIFO)的数据结构。元素只能在栈顶被添加或移除。

queue(队列):一种先进先出(FIFO)的数据结构。元素在队尾被添加,在队头被移除。

vector(动态数组):可以动态改变大小的数组

还有list(双向链表)、priority_queue(优先队列)、deque(双端队列)、and so on

2.2 关联容器

unordered_set 和 unordered_multiset:实现了基于哈希表的集合

unordered_map 和 unordered_multimap:实现了基于哈希表的映射

2.3 string(字符串)。

string用于存储和操作字符序列(即文本),它提供了各种对字符串进行操作的函数和方法。

3. 迭代器、算法

3.1 迭代器主要有:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器

3.2 STL算法主要分为非变更算法(findcountequal等)、变更算法(copyreplaceremove等)、排序算法(sortstable_sortpartial_sort等)和数值算法(accumulatemax_elementmin_element等)四大类。

七、输入/输出流

1. 流类和流对象

1.1 程序中,对数据的输入/输出是以字节流实现的。应用程序对字节序列作出各种数据解释。

1.2 流库是用继承方法建立的输入输出类库。流库具有两个平行的基类:streambuf 和 ios 类,所有流类均以两者之一作为基类。

2.标准流和流操作

2.1 标准流是C++预定义的对象,提供内存与外部设备进行数据交互功能。流的操作是流类的公有成员函数。

2.2 istream类的公有成员函数 :如get,getline ,operator>>

2.3 ostream类的公有成员函数 :如put,write,operator<<

2.4 设置标志字和格式控制符,用于格式控制

3.串流

3.1串流类是 ios 中的派生类,串流对象可以连接string对象或字符串

3.2 串流提取数据时对字符串按变量类型解释;插入数据时把类型数据转换成字符串

4.文件处理

4.1文件打开:包括建立文件流对象;与外部文件关联;指定文件的打开方式

4.2关闭文件操作:包括把缓冲区数据完整地写入文件,添加文件结束标志,切断流对象和外部文件的连接。若流对象的生存期没有结束,可以重用(文件管理,但是流还在)

4.3 操作流读指针的成员函数:seekg,tellg等

4.4 二进制流操作

八,异常捕获

1.try-catch

try { }: 表示要检查的部分——可能有异常的语句。

catch ( type ) { }: 用于捕获抛出的异常,并根据抛出的类型匹配需要进行的异常处理。type可以使用 …来表示任类型。

2.throw

throw 表达式;

如果在某段程序中发现了异常,就可以使用 throw 语句抛出这个异常。表达式:表示抛出的异常类型,异常类型由表达式的类型来表示。

3.带异常说明的函数原型

T func(para) throw(T1,T2);函数可以抛出T1,T2类型的异常

T func(para) throw();函数不抛出任何类型的异常

T func(para); 函数可以  抛出任何类型的异常

4.异常捕获的其他应用

多级异常传递。

创建对象的异常处理(在构造函数中发现问题直接throw)

And so on

学习资料及参考:

C++之继承最详讲_c++继承-CSDN博客

【C++】之多态最最最详细讲_智能指针 重写虚函数 既不相同,也不协变-CSDN博客

C++模板详解-CSDN博客

c++学习笔记_c++学习csdn-CSDN博客

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值