C++知识点总结(大纲)

C++概念

C++基本知识

C++基于过程

C++基于对象

C++面向对象

C++概念

C++是一门面向对象的编程语言,可以理解为是C语言的扩充,C语言为C++语言的子集,C++中增加了C语言中所没有的面向对象的特征。面向对象有三大特征,分别是封装继承多态
三大特征的概念:
封装:尽可能地隐藏对象的内部实现细节,控制用户对类的修改和访问的程度以及权限。(封装就是隐藏)
继承:继承可以使用不同的类的对象具有相同的行为;为了使用其他类的方法,我们没有必要重新编写这些旧方法,只要这个类(子类)继承包含的方法的类(父类)即可。 从下往上看,继承可以重用父类的功能;从上往下看,继承可以扩展父类的功能。
多态:多态可以使我们以相同的方式处理不同类型的对象。我们可以使用一段代码处理不同类型的对象,只要他们继承/实现了相同的类型。这样,我们没有必要为每一种类型的对象撰写相同的逻辑,极大的提高了代码的重用。

C++面向对象的技术实现:
封装:类、访问修饰符(public、protected、private)、
继承:继承(泛化)、组合(聚合)
多态:函数重载、函数重写、函数模板、类模板

======================================================================

C++基本知识

数据类型

基本数据类型:整型(int)、字符型(char)、实型{单精度型(float)、双精度型(double)}、布尔型(bool)、无值型(void)
非基本数据类型:数组(type [])、指针(type*)、引用(type&)、类(class)、结构体(struct)、枚举型(enum)、联合体(union)
注:不同的操作系统中,C++的数据类型所占用的字节数是不同的,可以通过sizeof操作符来判断在当前操作系统中数据类型所占用的字节数。

文字常量

当一个数值,例如 1,出现在程序中时,它被称为文字常量 (literal constant):称之为”文字“是因为我们只能以它的值的形式指代它,称之为“常量”是因为它的值不能被改变。每个文字都有相应的类型,例如 0是 int 型,而3.14159是double型的文字常量,是不可寻址的(nonaddressable),尽管它的值也存储在机器内存的某个地方,但是我们没有办法访问它们的地址。
整数文字常量可以被写成十进制,八进制或者十六进制的形式:
十进制无任何标识的数字常量,如8
八进制前缀为0,如010
十六进制前缀为0x,如0x8
缺省情况下,整数文字常量会被当做是int类型的有符号值。可以在文字常量的末尾添加“L”或“l”符号表示长整型,添加“U”或“u”符号表示无符号型。如:12UL、12ul、12Lu、12L等。
缺省情况下,浮点型文字常量会被当做是double类型的值。可通过”f“、”F”后缀来表示单精度类型。同时,可通过“L”、“l”后缀来扩展精度值。(注:“f"、"F"、”L“、”l“后缀只能用在十进制中)
字符型(char)文字常量:' '、'a'、'd'
字符串文字类型为常量字符数组:“a”、"hello"
布尔型(bool)文字常量:true、false
宽字符型(wchar_t)文字常量:L'a'
常量宽字符数组:L”hello“

变量

变量的概念:变量为我们提供了一个有名字的内存存储区,可以通过程序对其进行读、写和处理。C++中的每个符号变量都与一个特定的数据类型相关联,这个类型决定了相关内存的大小、布局、能够存储在该内存区的值的范围以及可以应用其上的操作集 。变量与常量的区别在于,变量是可寻址的。每个变量都与两个值相关联——1.它的数据值,存储在某个内存地址中,也可被称为右值;2.它的地址值,有时被称为左值,我们可以认为左值是地址值。
变量名:即变量的标识符,由数字、字符和下划线组成,但标识符首字符只能以字符或下划线开头。

const限定修饰符

const修饰变量时,可以将变量变成常量。被const修饰的变量必须在声明的同时初始化。如果有操作试图修改const修饰的变量,将发生编译错误。

指针

指针的概念:表示地址值。如果想要操作该指针地址所指向的内存空间的内容,需要解引用操作符“*”解除指针的引用。

数组

数组的概念:单一数据类型的集合。其中单个变量并未对其命名,可以通过其在数组中的位置进行访问,这种访问成为索引访问或下标访问。

数组与指针类型的关系:数组标识符代表数组中第一个元素的地址,它的类型是数组元素类型的指针。(如ia和&ia[0]是等价的,同样*ia和ia[0]也是等价的。)

字符串

C++提供了两种字符串的表示:C 风格的字符串和标准C++引入的string 类类型(一般建议使用string型字符串)。string类型由STL提供,将在STL专题中详解。

引用

引用的概念:引用(reference)有时候又称之为别名(alias),因为它可以看作是对象的另一个名字。引用类型在定义的时候必须被初始化,因为引用类型一旦引用被定义后,就不能再指向其它对象。
引用类型的定义形式:int a=1024;int &b=a;

枚举

枚举类型的定义形式:
enum type_name{one,two,three};//枚举类型定义后,type_name定义的变量类型只能为one,two,three。
type_name type_value=one;

联合体

/*联合体定义与使用*/
union typeName{
    char c;
    int i;
    int ii[10];
}
typeName t;

联合体变量的内存大小为该联合体类型包含的元素类型中占用字节最多的元素类大小。如上面的联合体类型中,元素c占用一个char大小,元素i占用一个int大小,元素ii占用十个int大小,所有该联合体类型typeName定义的变量t大小为十个int大小。

结构体

/*结构体定义与使用*/
struct typeName{
    char c;
    int i;
    int ii[10];
}
typeName t;

结构体的大小为结构体类型内包含所有元素的类型大小之和。

复数类型

复数类是C++标准库的一部分,包含在头文件#include<complex>中。
复数的概念:复数有两部分组成:实数部分和虚数部分。其表现形式如:3+4i
复数对象的定义:
complex c1(0,7);//表示0+7i,纯虚数
complex c2(3);//表示3+0i,虚数部分缺省为0
complex c3;//表示0+0i,实数部分和虚数部分都缺省为0
complex c4(c1);//用另一个虚数来初始化一个新的虚数对象

pair类型

pair类型是C++标准库的一部分,包含在头文件#include中。
pair类型的概念:
pair对象的定义:
pair<string,string> p1("name1","name2");
可以用成员访问符号“.”来访问pair单个成员的内容,如:p1.first;p1.second;

容器类型

容器的定义:在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对像的指针,这种对象类型就叫做容器。朴素地看,容器就是存储其它对象的对象。
C++的容器由C++标准模板库(STL)提供,STL 对定义的通用容器分三类:顺序性容器、关联式容器和容器适配器。
顺序性容器:顺序性容器是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。这个位置和元素本身无关,而和操作的时间和地点有关,顺序性容器不会根据元素的特点排序而是直接保存了元素操作时的逻辑顺序。(vector、deque、list
关联式容器:关联式容器和顺序性容器不一样,关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置 入容器时的逻辑顺序。但是关联式容器提供了另一种根据元素特点排序的功能,这样迭代器就能根据元素的特点“顺序地”获取元素。(set/multiset、map/multimap)
容器适配器:简单的理解容器适配器,其就是将不适用的序列式容器(包括 vector、deque 和 list)变得适用。(stack、queue、priority_queue
关于STL容器的总体与详细内容,将在《C++标准模板库(STL)详解》中介绍。

typedef修饰符

typedef的定义形式:typedef 数据类型 标识符;//如typedef int my_int;
typedef并未引入新类型,只是为数据类型引入了一个助记符。(与引用相似的地方在于,引用相当于为变量起别名,而typedef相当于为数据类型起别名。)

volatile限定修饰符

volatile修饰符与const的相似之处在于,它们都是附加修饰符。由于编译器会在处理源代码时对代码做一些优化,而有时候对于有些值这些优化将导致出错。volatile的主要作用在于提示编译器,不要武断地对被volatile修饰的值进行优化。

表达式

表达式由一个或多个操作数和N(N>=0)个操作符构成。

复数操作

条件操作符

操作形式:判断表达式?判断为真时执行此表达式:判断为假时执行此表达式;

sizeof操作符

作用:返回一个对象或类型名的字节长度,其返回值为size_t类型。
三种操作方式:

  1. sizeof(type_name);//如sizeof(类型名)
  2. sizeof(object);//如sizeof(对象名)
  3. sizeof object;//如sizeof 对象名

new和delete操作符

系统为每个程序都提供了一个在程序执行时可用的内存池,这个可用内存池被称为程序的空闲存储区(free store)或堆(heap)运行时刻的内存分配被称为动态内存分配(dynamic memory allocation)。动态内存分配由new表达式应用在一个类型指示符(specifier)上来完成。类型指示符可以是内置类型或用户定义类型。
如:int *a=new int;delete a;或者int *a=new int[n];delete[] a;

位操作符

操作符 功能 用法
~ 按位非 ~expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 按位与 expr1 & expr2
^ 按位异或 expr1 ^ expr2
` ` 按位或
&= 按位与赋值 expr1 &= expr2
^= 按位异或赋值 expr1 ^= expr2
` `= 按位或赋值
​ 00000101
^ 00000110
---------
​ 00000011

bitset类型

运算符优先级

类型转换

语法

声明语句

======================================================================

C++基于过程

域和生命周期

C++作用域的关键词:extern
C++生命周期的关键词:static
C++支持三种形式的域:局部域名字空间域类域
全局域:全局对象和非inline全局函数只能定义一次,inline函数可以被定义多次。
函数声明:函数声明(declaration)指定了该函数的名字以及函数的返回类型和参数表。
函数定义:函数定义(definition)包含了函数声明部分,同时还为函数提供了函数体。
extern关键词:修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间。extern “c”表示C语言代码。
在局部域中的变量声明引入了局部对象。有三种局部对象:自动对象、寄存器对象(register关键词)以及局部静态对象(static关键词)。

函数

概念:函数由返回值类型函数名参数列表函数体构成。
函数形式:返回值类型 函数名(参数列表){函数体}
函数原型:函数原型由返回值类型、函数名和参数列表构成。

函数传参

指针参数和引用参数的区别:指针可以指向任何对象,也可以不指向任何对象,在知道指针指向一个有效对象之前不能轻易的对指针进行解引用;引用则必须明确的指向一个对象,且一旦指向之后就不可以更改,否则就会报错。
例如:

void f(int* a,int& b){
    if(!a)std::cout<<*a<<std::endl;//当参数为指针时,必须先判断该指针是否指向一个有效对象,再使用。
    std::cout<<b<<std::endl;//当参数为引用时,可以直接使用
}
int main(){
    int i=0;
    f(&i,i);//正确
    f(nullptr,i);//正确
    f(nullptr,0);//错误表示!引用参数必须为一个对象,不能是常量
    return 0;
}

所以,如果一个参数可能在函数中指向不同的对象,或者不指向任何对象,则必须使用参数指针。当一个参数在函数中只指向一个确定的对象,则使用引用参数。

重载函数

概念:在同一作用域内,一组函数的函数名相同参数列表不同(参数个数不同/参数类型不同),返回值可同可不同。
函数重载:
1、具有相同的名称,执行基本相同的操作,但是使用不同的参数列表。
2、函数的多态性。
3、编译器通过调用时参数的个数和类型确定调用重载函数的哪个定义。
4、只有对不同的数据集完成基本相同任务的函数才应重载。
函数重载的优点:
1、减少了函数名的数量
2、增加了代码的可读性,有助于理解和调试代码
3、易于维护代码
函数重载是一种静态多态。(关于多态的详细全面的内容,将在《C++三大特性(封装、继承和多态)及其技术实现详解》一文中详解)
C++函数重载的原理:
函数重载解析:把函数调用与重载函数集合中的一个函数相关联的过程 。
编译器在编译.cpp文件中当前使用的作用域里的同名函数时,根据函数形参的类型顺序会对函数进行重命名(不同的编译器在编译时对函数的重命名标准不一样)但是总的来说,他们都把文件中的同一个函数名进行了重命名;

  • 在vs编译器中:
    根据返回值类型(不起决定性作用)+形参类型和顺序(起决定性作用)的规则重命名并记录在map文件中。
  • 在linux g++ 编译器中:
    根据函数名字的字符数+形参类型和顺序的规则重命名记录在符号表中;从而产生不同的函数名,当外面的函数被调用时,便是根据这个记录的结果去寻找符合要求的函数名,进行调用;
    为什么C语言没有函数重载机制?
    编译器在编译.c文件时,只会给函数进行简单的重命名;具体的方法是给函数名之前加上”_”;所以加入两个函数名相同的函数在编译之后的函数名也照样相同;调用者会因为不知道到底调用那个而出错;

函数模板

关键字:template、class、typename
例:

/*a.h*/
template <class Type,int size>
Type functionName(Type (&type_name)[size]){}//当调用此函数时,需要在Type传入数据类型,size位置传入int型常量
/*b.c*/
#include "a.h"
int main(){
    int ia[5]={1,2,3,4,5};
    /*函数模板实例化*/
    int i=functionName(ia);//该模板函数的Type为int,size为5.
    return 0;
}

模板编译模式分为:分离编译模式、包含编译模式
包含编译模式:函数模板的声明和定义都放在头文件中。
分离编译模式:函数模板的声明放在头文件中,定义放在源文件中。
显式实例化声明:
如:

/*函数模板*/
template <class T>
T f(T t,int i);
/*显式实例化声明*/
template int f(int t,int i);

模板显式特化:
定义:

异常处理

关键字:try、catch、throw
概念:异常是程序可能检测到的,运行时刻不正常的情况,如除0、数组访问越界或存储内存耗尽等。

泛型算法

======================================================================

C++基于对象

类定义和访问权限

类声明
类声明的形式:class 类名;
例如:class Vector;
声明一个Vector类类型,但是由于未定义,不知道该类类型的大小,编译器不知道为该类类型的对象预留多少空间,所以也无法定义该类类型对象。但是,可以声明一个指向该类类型的指针或引用,因为指针和引用的内存大小与所指向的对象大小无关。
类定义
类定义包含两部分:类头,由关键字class和类名构成;类体,花括号内内容。
类体定义了类成员表,类成员表包括数据成员成员函数
成员访问
信息隐藏:防止程序的函数直接访问类的内部信息而提供的一种机制。通过访问限定符(public、protected、private)来实现。
公有成员(public):在程序的任何地方都可以被访问。
私有成员(private):只能被成员函数和类的友元访问。
被保护成员(protected):派生类可访问,非派生类不可访问。

友元访问

在某些情况下,允许某个函数而不是整个程序可以访问类的私有成员,这样做会比较方便,友元机制允许一个类授权其他的函数访问它的非公有成员。友元声明以关键字 friend 开头,它只能出现在类的声明中。
友元可分为:友元类友元函数
详细内容见友元访问在操作符重载的应用

类的初始化、赋值和析构

类的构造函数

关键词:explicit、inline、const、volatile
构造函数概念:类的构造函数与类名同名,且无返回值类型。C++可以有多个构造函数,唯一的要求是这些函数参数列表不能相同。一般构造函数用来初始化类中的数据成员,对于有些需要传参的构造函数,当参数经常使用默认值时,可以使用缺省参数。
例如:

/*例1:构造函数使用缺省参数的类*/
class Account{
public:
    //缺省构造函数
    Account(){}
    Account(int i,int j=111,int x=111):i_(i),j_(j),x_(x){std::cout<<"使用缺省实参的构造函数"<<std::endl;}
private:
    int i_;
    int j_;
    int x_;
};
/*例2:使用普通构造函数的类*/
class Account{
public:
    //缺省构造参数
    Account(){}
    Account(int i,int j,int x):i_(i),j_(j),x_(x){std::cout<<"普通构造函数"<<std::endl;}
    Account(int i,int j):i_(i),j_(j){
        x_=111;
    }
    Account(int i):i_(i){
        j_=111;
        x_=111;
    }
private:
    int i_;
    int j_;
    int x_;
};

例1和例2中的类的构造函数结果是等价的,都可以传入一个参数,两个参数,或者三个参数。但是从效果上来看,例1中利用缺省参数的构造函数更好,节省代码量,也利于理解。

构造函数重点:缺省构造函数限制对象创建拷贝构造函数
1、缺省构造函数:没有参数,或参数都是默认值。一个类最多有一个缺省构造函数。
2、限制对象创建:将某些构造函数声明为private型,从而限制通过某些构造函数创建对象。
3、拷贝构造函数:用一个类对象初始化该类的另一个对象。

拷贝构造函数:
拷贝构造函数就是用一个类对象初始化该类的另一个对象,但是拷贝构造函数的参数必须是同一个类的引用对象
一个对象的拷贝构造函数可以访问被拷贝的对象的私有成员,例如:

class Demo{
public:
    Demo(){}
    Demo(const Demo& demo):i(demo.i){"std::cout<<拷贝构造函数<<std::endl;"}//拷贝构造函数,参数必须是Demo&类型。
private:
    int i;
}

类的数据成员初始化

类中的数据成员能否在定义类的时候就初始化?

存在问题,仍未解决~

class Demo{
public:
    Demo(){}
private:
    int i=0;//此处在定义类的时候初始化
}

答案是不能在此处初始化。原因在于:1、类的定义实际上相当于类型的声明,里面的数据成员都是声明,类型声明并没有分配内存空间,所以无法存放该类型;2、假如类是抽象类,那么在类中特征初始化将破坏类的抽象性。

  • const成员:也就是常量成员,它们在声明时就要初始化,因为它们为常量成员,一旦定义,就不能修改
  • 引用成员&:引用代表的就是被引用者自身,是变量的别名,所以引用类型变量一旦初始化,它们也不能修改
  • const、引用成员必须在类的构造函数中的初始化列表中初始化,不能在构造函数的函数体中进行赋值
  • static成员:静态成员,它不像其他成员,static没有隐藏的this指针,所以static成员是属于整个类的,不是某个对象的,静态成员的初始化要在类外初始化,在类外初始化时需要隐藏static关键字,只有整形静态常量可以在类内初始化
  • 整型静态常量:static const int 类型,这种数据类型可以在类内初始化
class A{
public:
    const int a; // 常量成员,需要在构造函数的初始化列表中赋值初始化
    int& b; // 引用成员,也是构造函数的初始化列表赋值
    static int c; // 静态成员,要在类外初始化
    static const int d = 0; // 静态整形,可以在类内初始化
    static int f;
    static const double e; // 只能在类外初始化
    A(int a, int& b ):a(a), b(b){}
}
// 需要在类外初始化的成员变量,隐藏static关键字
    const double A::e = 1.0;
    const int A::f = 1; // 也可在类外初始化

类的析构函数

释放在该类内申请的动态资源。

重载操作符和用户定义的转换

操作符重载

形式:返回值 operator操作符(参数列表){函数体}
注意:重载操作符的函数参数列表中必须至少有一个操作数为类类型。
例如:

/*将<<操作符重载为用以计算两个int类型值之和,这种是错误的,因为两个重载操作符的函数两个参数都是非类的数据类型*/
int operator<<(int i,int j){
    return i+j;
}
int main(){
    int i=111,j=222;
    int sum=i<<j;//相当于int sum=operator<<(i,j);
    return 0;
}
/*将<<操作符重载为用以计算int变量和A类对象数据成员j的和,并返回int类型结果,这种是正确的,因为该操作符重载函数有一个参数为类类型*/
class A{
public:
    A(int _j):j(_j){}
    int j;
};
int operator<<(int i,A j){
    return i+j.j;
}
int main(){
    int i=111;
    A a(222);
    int sum=i<<a;//相当于int sum=operator<<(i,a);
    cout<<sum<<endl;
    return 0;
}

友元访问在操作符重载的应用

友元访问分为:
1、友元类访问:将一个类声明为另一个类的友元类。如:

class Demo1{}
class Demo{
    friend class Demo1;
public:/*……*/
private:/*……*/
};

2、友元函数访问:将一个函数声明为另一个类的友元函数。如:

void f(){}
class Demo{
    friend void f();
public:/*……*/
private:/*……*/
};

操作符重载分为几种不同情况,对应几种不同的操作符重载函数,需要不同的编程技巧:
情况一:重载操作符函数为全局函数,形式上是两个操作数,函数参数也是两个操作数,且两个操作符可以为任意变量类型。
在这种情况下,当操作数有类类型时,在函数内如果想要直接访问类内成员需要设置为该类的友元函数(当然也可以不访问)。例如:

/*
重载+操作符,左边为基本数据类型,右边为类类型,返回值为类类型。
形式:类类型=基本数据类型+类类型;
*/
class Demo{
public:
    friend Demo operator+(int i,Demo& j);//声明操作符重载函数为类Demo的友元函数
    Demo(int _i):i(_i){}
    int getValue(){return i;}
private:
    int i;
};
/*操作符重载函数*/
Demo operator+(int i,Demo& j){
    Demo sum(i+j.i);//其实也未必需要将操作符重载函数设置为Demo类的友元函数,因为类的数据成员可以间接访问,可以直接将sum(i+j.i)改为sum(i+j.getValue())即可。
    return sum;
}

int main(){
    int d1=111;
    Demo d2(222);
    Demo d3=d1+d2;//此时相当于调用了全局函数operator+(d1,d2),相当于将形式上两个操作数作为两个参数传入了全局函数中。
    cout<<d3.getValue()<<endl;
    return 0;
}

情况二:重载操作符函数为成员函数,形式上是两个操作数,函数参数是一个操作数,但是形式上的左操作数必须是重载操作符函数的类,右操作数可以为任意类型。

class Demo{
public:
    Demo operator+(int i){//重载操作符函数为成员函数
        Demo sum(this->i+i);
        return sum;
    }
    Demo(int _i):i(_i){}
    int getValue(){return i;}
private:
    int i;
};

int main(){
    int d1=111;
    Demo d2(222);
    Demo d3=d2+d1;/*相当于调用了Demo类的成员函数d2.operator+(d1),所以不能写成d1+d2,否则会编译错误。因为d1是int类型,而重载操作符函数是Demo的类成员函数 。形式上两个操作数,实际上是一个操作数对象调用了成员函数,并将另一个操作数作为参数传入。如果想要d1+d2合法,这时需要些一个全局函数来重载操作符+*/
    return 0;
}

类模板

类模板的定义与实例化

模板类的定义形式:

template<class T1,typename T2,int size>
class Demo{
public:
    void prin(){
        for(int i=0;i<size;i++)std::cout<<t1[i]<<std::endl;
        for(int i=0;i<size;i++)std::cout<<t2[i]<<std::endl;
    }
private:
    T1 t1[size];
    T2 t2[size];
}

模板类型参数:用关键词class或typename声明。当实例化时会用数据类型标识符替换,例如T1替换成int。
模板非类型参数:用数据类型标识符声明。当实例化时会用该数据类型的常量替换,例如size替换成int类型的常量10。
总结:模板类型参数相当于以数据类型为参数,通俗的说就是把数据类型当成类模板的变量;而模板非类型参数相当于以数据类型的常量表达式为参数,通俗来讲就是把常量表达式的计算结果(常量表达式计算结果为常量)当成类模板的变量
将模板类型参数与非类型参数替换成具体的数据类型与数据类型常量的过程,称为模板实例化
类模板的参数可以有缺省实参,这对类型参数和非类型参数都一样。就像函数参数的缺省,模板参数的缺省是一个类型或一个值。如下示例:

template<class T1,class T2=string,int size=1024>//T1缺省为string类型,size缺省为计算结果为int类型的常量表达式1024
class Demo{
public:
    Demo(T1 _t1,T2 _t2):t1(_t1),t2(_t2){}
    void printValue(){
        std::cout<<"t1="<<t1<<std::endl;
        std::cout<<"t2="<<t2<<std::endl;
        std::cout<<"size="<<size<<std::endl;
    }
private:
    T1 t1;
    T2 t2;
};

int main(){
    int i=1234;
    Demo<string> demo("sss","mmm",i);//此处可以传入变量i,是因为此处非类型参数以常量表达式的计算结果为参数。
    demo.printValue();
    return 0;
}

注意:在参数列表中,缺省参数右边必须都是缺省参数,否则编译器将报错。
从通用的类模板定义生成类的过程被称为模板实例化

在模板实参的类型和非类型模板参数的类型之间允许进行一些转换,能被允许的转换集
是“函数实参上被允许的转换”的子集:
1、左值转换,包括从左值到右值的转换、从数组到指针的转换,以及从函数到指针的转换。例如:

template <int *ptr> class Demo{};
int array[10];
Demo<array> demo;//从数组到指针的转换

2、限定修饰符转换。例如:

template <const int* ptr> class Demo{};
int i;
Demo<&i> demo;//从int*类型到const int*类型的转换 

3、提升。例如:

template <int i> class Demo{};
const short int ii;
Demo<ii> demo;//从short到int类型的提升

4、数值转换。例如:

template <unsigned int size> Demo{};
Demo<1024> demo;//从int类型到unsigned int类型

类模板中的友元声明

有三种友元声明可以出现在类模板中:
1、非模板友元类或友元函数。友元类和友元函数都是非模板的。
例如:

class Foo {//非模板类
    void bar();//非模板类的非模板成员函数
};
void foo();//非模板函数
template <class T>
class QueueItem {
    friend class foobar;//非模板友元类
    friend void foo();//非模板友元函数
    friend void Foo::bar();//非模板友元类的非模板成员函数
    // ...
};

2、绑定的友元类模板或函数模板。友元类和友元函数都是模板的,但是已经与实例化模板函数/类绑定,相当于该友元只能是某种实例化的模板函数/类。

template <class Type>
class foobar{ ... };//类模板

template <class Type>
void foo( QueueItem<Type> );//函数模板

template <class Type>
class Queue {
    void bar();//类模板的成员函数模板
// ...
};

template <class Type>
class QueueItem {
friend class foobar<Type>;//绑定的模板友元类
friend void foo<Type>( QueueItem<Type> );//绑定的模板友元函数
friend void Queue<Type>::bar();//绑定的模板类友元成员函数
// ...
};

3、非绑定的友元类模板或函数模板

template <class Type>
class QueueItem {
    template <class T>
    friend class foobar;//非绑定的模板友元类
    
    template <class T>
    friend void foo( QueueItem<T> );//非绑定的模板友元函数
    
    template <class T>
    friend void Queue<T>::bar();//非绑定的模板类友元成员函数
    // ...
};

注意:友元的目的是为了访问友元声明时所在类的私有数据成员,不改变该类的数据成员与成员函数的数量或其它。

类模板的静态数据成员

======================================================================

C++面向对象

类继承和子类型

基类和派生类的定义

基类必须被定义才能作为基类。例如:

class Parent;
class Child:public Parent{/*……*/};//错误,基类Parent必须被定义,只有声明是无效的

前向声明不能包含派生类的派生表。例如:

class Parent{};
class Child:public Parent;//错误,Child类为前向声明,还未定义,前向声明不能包含派生类的派生表

派生类的前向声明正确的方法是:

class Parent{};
class Child;

基类的构造:
1、基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
2、成员类对象构造函数。如果有多个成员类对象,则构造函数的调用顺序是对象在类中,被声明的顺序,而不是它们出现在成员初始化表中的顺序。
3、派生类构造函数。作为一般规则,派生类构造函数应该不能直接向一个基类数据成员赋值,而是把值传递,否则基类和派生类会变成“紧耦合”,当修改基类数据成员时会影响派生类。
基类与派生类构造函数

class Parent{
public:
    Parent(int i):_i(i){}
private:
    int _i;
};

class Child:public Parent{
public:
    Child(int i,int j):Parent(i),_j(j){}
private:
    int _j;
};

还为了解的点:迟缓型错误检测

继承关系中类的构造函数与析构函数的调用顺序

构造函数分为:父类的构造函数,父类数据成员的构造函数,子类的构造函数,子类数据成员的构造函数。
构造函数的调用顺序为
1、父类数据成员的构造函数;
2、父类的构造函数;
3、子类数据成员的构造函数;
4、子类的构造函数。
析构函数的调用顺序完全相反。
另外,当父类或子类拥有多个数据成员时,数据成员构造函数的调用顺序与父类或子类构造函数的成员初始化表的顺序无关,而与类中定义该数据成员的顺序有关。
例如:

class Demo{
public:
    Demo(string s):_s(s){cout<<"Demo构造函数:"<<s<<endl;}
    ~Demo(){cout<<"Demo析构函数"<<_s<<endl;}
private:
    string _s;
};

class Parent{
public:
    Parent(int i):_i(i),d_parent2("d_parent2"),d_parent1("d_parent1"){cout<<"Parent构造函数"<<endl;}
    ~Parent(){cout<<"Parenet析构函数"<<endl;}
private:
    int _i;
    Demo d_parent1;
    Demo d_parent2;
};

class Child:public Parent{
public:
    Child(int i,int j):d_child2("d_child2"),Parent(i),_j(j),d_child1("d_child1"){cout<<"Child构造函数"<<endl;}
    ~Child(){cout<<"Child析构函数"<<endl;}
private:
    int _j;
    Demo d_child1;
    Demo d_child2;
};

int main(){
    Child c(1,2);
    return 0;
}

运行结果是:

Demo构造函数:d_parent1
Demo构造函数:d_parent2
Parent构造函数
Demo构造函数:d_child1
Demo构造函数:d_child2
Child构造函数
Child析构函数
Demo析构函数d_child2
Demo析构函数d_child1
Parenet析构函数
Demo析构函数d_parent2
Demo析构函数d_parent1

Process returned 0 (0x0)   execution time : 0.742 s
Press any key to continue.

基类与派生类的虚拟函数

缺省情况下,类的成员函数是非虚拟的(nonvirtual)。当一个成员函数为非虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,就是该类对象的静态类型中定义的成员函数。例如:

class Parent{
public:
    void demo(){cout<<"Parent"<<endl;}
};
class Child:public Parent{
public:
    void demo(){cout<<"demo"<<endl;}
};

int main(){
    Parent *p=new Child;//定义p的静态类型为Parent类型,而分配内存的动态类型为Child
    p->demo();//通过类对象的指针调用该函数
    Parent &cp=*p;
    cp.demo();//通过类对象的引用调用该函数
    return 0;
}

此时的执行输出结果为:
Parent
Parent

纯虚函数:没有函数体的函数声明=0。例如:virtual void demo()=0;

虚函数可以通过动态联编动态调用,也可以静态调用,例如:

class Parent{
public:
    virtual void demo(){cout<<"Parent"<<endl;}
};
class Child:public Parent{
public:
    void demo(){cout<<"demo"<<endl;}
};

int main(){
    Parent *p=new Child;
    p->Parent::demo();//虚拟函数的静态调用,在编译时刻解析,调用的类型为定义指针的类型
    p->demo();//虚拟函数的动态调用,在运行时刻动态联编,调用的类型为指针所指向地址的对象类型
    return 0;
}

虚析构函数

当我们通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。
例如:

/*基类的析构函数没有被声明为虚函数时*/
class Parent{
public:
    Parent(){cout<<"Parent构造函数"<<endl;}
    ~Parent(){cout<<"Parent析构函数"<<endl;}
};
class Child:public Parent{
public:
    Child(){cout<<"Child构造函数"<<endl;}
    ~Child(){cout<<"Child析构函数"<<endl;}
};

int main(){
    Parent* p=new Child;
    delete p;//通过基类的指针来销毁对象,此时父类的析构函数不是虚函数
    return 0;
}

运行结果为:

Parent构造函数
Child构造函数
Parent析构函数
Process returned 0 (0x0)   execution time : 0.647 s
Press any key to continue.
/*基类的析构函数被声明为虚函数时*/
class Parent{
public:
    Parent(){cout<<"Parent构造函数"<<endl;}
    virtual ~Parent(){cout<<"Parent析构函数"<<endl;}
};
class Child:public Parent{
public:
    Child(){cout<<"Child构造函数"<<endl;}
    ~Child(){cout<<"Child析构函数"<<endl;}
};

int main(){
    Parent* p=new Child;
    delete p;//通过基类的指针来销毁对象,此时父类的析构函数是虚函数
    return 0;
}

运行结果为:

Parent构造函数
Child构造函数
Child析构函数
Parent析构函数
Process returned 0 (0x0)   execution time : 0.485 s
Press any key to continue.

可以得出结论,将基类的析构函数声明为虚函数常常是必要的,否则销毁对象时可能会导致析构不完整。

未完待续……

posted @ 2021-04-02 13:45  一只小菜菜鸟  阅读(3425)  评论(2编辑  收藏  举报