C++面向对象特性之多态

一种对象为了接口重用而呈现的多重形态
静态多态有模板重载,根据不同的参数调用函数体不同,这是在编译是就确定的
动态多态有父子类中的虚函数,父类的指针或者引用调用的虚函数如果进行了重写则会调用子类的虚函数,这是通过动态连联编运行时确定的
静态绑定和动态绑定

绑定体现了函数调用和函数本身代码的关联,也就是产生调用时如何找到提供调用的方法入口:

静态绑定:程序编译过程中把函数调用与执行调用所需的代码相关联,这种绑定发生在编译期,程序未运行就已确定,也称为前期绑定。

动态绑定:执行期间判断所引用对象的实际类型来确定调用其相应的方法,这种发生于运行期,程序运行时才确定响应调用的方法,也称为后期绑定。

在C++泛型编程中可以基于模板template和重载override两种形式来实现静态多态。动态多态主要依赖于虚函数机制来实现,不同的编译器对虚函数机制的实现也有一些差异。现代的C++编译器对于每一个多态类型,其所有的虚函数的地址都以一个表V-Table的方式存放在一起,虚函数表的首地址储存在每一个对象之中,称为虚表指针vptr,这个虚指针一般位于对象的起始地址。通过虚指针和偏移量计算出虚函数的真实地址实现调用。

当派生类有多个基类,在派生类中将出现多个虚表指针,指向各个基类的虚函数表,在派生类中会出现非覆盖和覆盖的情况。虚继承将共同基类设置为虚基类,从不同途径继承来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。从而解决了二义性问题、节省了内存,避免了数据不一致的问题。

如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类,否则仍然为抽象类。

实现多态的基类析构函数一般被声明成虚函数,如果不设置成虚函数,在析构的过程中只会调用基类的析构函数而不会调用派生类的析构函数,从而可能造成内存泄漏。

如果某个类将作为基类或者包含虚函数那么建议使用虚析构。

「多态」的关键在于通过基类指针或引用调用一个虚函数时,编译时不能确定到底调用的是基类还是派生类的函数,运行时才能确定。每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着虚函数表的指针。「虚函数表」中列出了该类的「虚函数」地址。多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址并调用虚函数的指令。一个类如果定义了虚函数,则应该将析构函数也定义成虚函数;或者打算作为基类使用,也应该将析构函数定义成虚函数。构造函数不能定义成虚构造函数。

重载运算符

运算符重载的实质就是函数重载,可以重载为普通函数,也可以重载为成员函数。运算符重载的基本形式如下:

返回值类型 operator运算符(形参表)
{...}

重载为成员函数时,参数个数为运算符数减一。如: c=a-b; 等价于c= a.operator-(b)
重载为普通函数时,参数个数为运算符数。如: c=a+b;等价于c=operator+(a,b)
如果参数表如果普通的对象形式Complexc,那么在入参的时候,就会调用默认的赋值
(拷贝)构造函数,产生临时对象增大开销,所以采用引用的方式,同时为了防止引用的对象被修改,所以就定义成了const Complex &c常引用类型。
 

1、引入运算符重载,是为了实现类的多态性;只能重载已有的运算符;对于一个重载的运算符,其优先级和结合律与内置类型一致才可以;不能改变运算符操作数个数;
2、.    ::   ?:  sizeof   typeid  **不能重载;
3、两种重载方式,成员运算符和非成员运算符,成员运算符比非成员运算符少一个参数;下标运算符、箭头运算符必须是成员运算符;
5、当重载的运算符是成员函数时,this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个;至少含有一个类类型的参数;
6、当运算符既是一元运算符又是二元运算符(+,-,*,&),从参数的个数推断到底定义的是哪种运算符,
7、下标运算符必须是成员函数,下标运算符通常以所访问元素的引用作为返回值,同时最好定义下标运算符的常量版本和非常量版本;
8、箭头运算符必须是类的成员,解引用通常也是类的成员;重载的箭头运算符必须返回类的指针;
函数重载时,形参无法通过const区分,但指针引用形参可以通过const区分。当传入非常量类型实参到重载函数时,优先调用非常量。对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其他所有的要素都相同。假设有两个函数,它们的形参列表一样但是返回类型不同,则第二个函数的声明是错误的:.
Record lookup (const Account&) ;
bool lookup (const Account&); // 错误:与上一个函数相比只有返回类型不同

函数模板

函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
函数模板的工作原理/工作机制
编译器从函数模板通过具体类型产生不同的函数,对函数模板进行两次编译:在声明的地方对模板代码本身进行编译;在调用的地方对参数替换后的代码进行编译。
为什么函数模板和普通函数能进行重载
因为编译器对函数模板进行二次编译,生成类型对应的函数,和普通函数构成重载的关系;故函数模板和普通函数能实现重载的作用。
重载和函数模板的区别
重载需要多个函数,这些函数彼此之间函数名相同,但参数列表中参数数量和类型不同。 
模板函数是一个通用函数,函数的类型和形参不直接指定而用虚拟类型来代表。但只适用于参数个数相同而类型不同的函数。
一个比较大小的模板函数

#include<iostream>  
using namespace std;  
template<typename type1,typename type2>//函数模板  
type1 Max(type1 a,type2 b)  
{  
    return a > b ? a : b;  
}  
void main()  
{  
cout<<"Max = "<<Max(5.5,'a')<<endl;  }

在有多个函数和函数模板名字相同的情况下,一条函数调用语句应该被匹配成哪个函数或哪个模板的调用,C++编译器遵循以下优先顺序。
(1)先找参数完全匹配的普通函数(非由模板实例化而得的函数)。
(2)再找参数完全匹配的模板函数。
(3)然后找实参数经过自动类型转換后能够匹配的普通函数。
(4)上面的都找不到,则报错.

类模板

类模板用于解决多个功能相同、数据类型不同的类需要重复定义的问题。只要为这一批类创建一个类模板,就可以用来生成具体的类。

template<typename T1,typename T2>
class univer_student_template{
    private:
        T1 name;
        T2 score;
    public:
        univer_student_template()=default;
        univer_student_template(T1 name_ ,T2 score_ ):name(name_),score(score_){};
        univer_student_template(const univer_student_template<T1,T2>&stu1){name=stu1.name;score=stu1.score;};          
        void print_info();
};
template<typename T1,typename T2>
void univer_student_template<T1,T2>::print_info()
{
    cout<<"name: "<<name<<endl;
    cout<<"score: "<<score<<endl;
}

模板类和模板函数的区别
函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加<T>,而函数模板不必

类模板并不是类和成员函数的定义,模板只是说明如何生成类和成员函数。在编译的时候,编译器发现类模板并不立即产生代码。只有在类模板实例化为模板类的时候(例如,创建了一个模板类对象),编译器才会产生特定类型的模板实例。

template<typename T>
class Queue
{
    public:
    Queue(){}
    ~Queue(){}
};
上面声明类模板Queue是一个生成类的方案,并不是生成类的具体定义,不会产生类代码。
Queue<int> qul;
Queue<double> qu2;
看到上述声明后,编译器将按Queue模板来生成两个独立的类定义。类声明Queue将使用int代替模板中所有的T,
而类声明Queue将使用double代替模板中所有的T,也称模板实例化。

模板特化(template specialization)不同于模板的实例化,模板参数在某种特定类型下的具体实现称为模板特化。模板特化有时也称之为模板的具体化,分别有函数模板特化和类模板特化。
模板偏特化(Template Partitial Specialization)是模板特化的一种特殊情况,指显示指定部分模板参数而非全部模板参数,或者指定模板参数的部分特性而非全部特性,也称为模板部分特化。与模板偏特化相对的是模板全特化,指对所有的模板参数进行特化。模板全特化与模板偏特化共同组成模板特化。
模板偏特化主要分为两种,一种是对部分模板参数进行全特化,另一种是对模板参数特性进行特化,包括将模板参数特化为指针、引用或是另外一个模板类。假如我们有一个compare函数模板,在比较数值大小时没有问题,如果传入的是数值的地址,需要比较两个数值的大小,而非比较传入的地址大小。
对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:全特化类>偏特化类>主版本模板类。这样的优先级顺序对性能也是最好的。


标准库

1)C++ 标准库可以分为两部分:
标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言。
面向对象类库: 这个库是类及其相关函数的集合。
2)输入/输出 I/O、字符串和字符处理、数学、时间、日期和本地化、动态分配、其他、宽字符函数
3)标准的 C++ I/O 类、String 类、数值类、STL 容器类、STL 算法、STL 函数对象、STL 迭代器、STL 分配器、本地化库、异常处理类、杂项支持库

输入/输出 I/O

 

printf函数

在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了。      

cout和printf有什么区别?
cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。
cout是有缓冲输出:
cout < < "abc " < <endl;
或cout < < "abc\n ";cout < <flush; 这两个才是一样的.
endl相当于输出回车后,再强迫缓冲输出。
flush立即强迫缓冲输出。
printf是无缓冲输出。有输出时立即输出

cin对应于标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据。cin是istream的对象 表示标准输入。<<是istream的成员运算符函数表示写入数据
cout对应于标准输出流,用于向屏幕输出数据,也可以被重定向为向文件写人数据。cout是ostream的对象 表示标准输出。>>是ostream表示读出数据。
cerrcerr是ostream对象 表示标准错误。对应于标准错误输出流,用于向屏幕输出出错信息,不能被重定向。
clog对应于标准错误输出流,用于向屏幕输出出错信息,不能被重定向。cerr和clog的区别在于cerr不使用缓冲区,直接向显示器输出信息;而输出到clog中的信息先会被存放在缓冲区,缓冲区满或者刷新时才输出到屏幕。

缓冲区相关问题:
默认情况下在写比如cout<<"hello"的时候,经过的过程是 字符串->缓冲区->设备(屏幕)为什么不直接输出到设备(屏幕)呢?
因为设备的写操作很费时,所以操作系统把所有的输出流命令先攒到缓存区,等缓存区满的时候就采用系统级写操作一下子输出到设备,这样操作系统的效率会高很多

怎么使字符串一输入到缓冲区就直接写入设备(缓存区刷新)?
1.程序正常结束
2.缓冲区满
3.用缓冲区刷新操纵符--endl,flush,ends
4.用unibuf操纵符
5.关联输入和输出流

cout << "hi!" <<endl; //输出hl和一个换行,然后刷新缓冲区
cout << "hi!" <<flush; //输出hi.然后刷新城冲区,不附加任何额外字符
cout << "hi!" <<enda;//输出hi和一个空字符,然后刷新缓冲区
使用unitbuf操作符在每次输出操作后都刷新缓冲区。它告诉流在接下来的每次写操作之后都进行一次
flush操作。而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:
cout << unitbuf;//所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf; //回到正常的缓冲方式

流类不能拷贝赋值给别的对象,所以流类不能作为函数形参,所以要想在函数参数中用流类,要以引用的形式传进去。而且由于读写一个IO对象时,其状态会改变,所以不能设置为const类型

IO库就是由一些标准IO类组成的类库,通过这些IO类可以实现对设备(控制台,文件)的IO操作,对内存的IO(即String类)操作,即实现控制台IO,文件IO,内存IO。其中对控制台的IO操作一般通过iostream头文件中的流类,对文件的IO操作一般通过fstream头文件中的流类,对内存string的IO操作一般通过sstream头文件中的流类。

文件流

<ifstream><ofstream><fstream>系列头文件和控制台IO的继承关系如上图,所以继承了它们的成员函数和成员变量。一般要进行文件读写直接包括进来一个<fstream>就可以了

//用流类的构造函数打开文件
#include<iostream>
#include<fstream>
using nanespace std;
int nain()
{
    ifstrean inFile;
    inFile.open("c:\\tmp\\test.txt" , ios::in);
    if(inFile)    //条件成立则说明文件打开成功
        inFile.close();
    else
        cout << "test.txt doesn't exist" << endl; :
    ofstrean oFile;
    oFile.open( "test1.txt", ios::out);
    if(!oFile)//条件成立则说明文件打开出错
        cout<< "error 1" << endl;
    else
        oFile.close();
    oFile.open("tmp\\test2.txt",ios::out | ios:: in);
    if(oFile) !//条件成立则说明文件打开成功
        oFile.close();
    else
        cout<<"error 2" << endl;
    fstream ioFile;
    ioFile.open("..\test3.txt", ios::out | ios::in | ios:: trunc);
    if(!ioFile)
        cout<< "error 3" < end1;
    else
        ioFile.close();
    return 0;
}

创建文件流对象时可以传进去文件名,这样该对象会自动调用open函数,文件名既可以是string对象,也可以是c风格字符串
在要求基类对象的地方可以用集成对象代替,所以在接受iostream类型引用对象的地方可以用fstream或者sstream对象代替,即函数形参是&iostream 可以传进去fstream或者sstream
调用open可能会失败 所以在对一个文件流使用open函数后应该对其进行检测是否成功打开:
ostream out;
out.open(file1);
if(out)//如果打开成功则条件为真
{...}
out.close;
out.open(file2);
当一个文件流open失败,我们要把它绑定到另外一个的时候,应该先关闭再打开另一个

当一个fstream离开其作用域的时候该对象会被自动销毁,与之关联的文件会自动关闭.

对于一个给定流,每当打开文件时,都可以改变其文件模式。
ofstream out;  //未指定文件打开模式
out.open("scratchpad");   //模式隐含设置为输出和截断
out.close();   //关闭out,以便我们将其用于其他文件
out.open("precious", ofstream::app); //模式为输出和追加
out.close() ;
第一个open调用未显式指定输出模式,文件隐式地以out模式打开。通常情况下,out模式意味着同时使用trunc模式。因此,当前目录下名为scratchpad的文件内容将被清空。当打开名为precious的文件时,指定了append模式。文件中已有的数据都得以保留,所有写操作都在文件末尾进行。
     

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值