高级程序设计-C++

目录

内存模型

代码区:

全局区:

栈区

堆区

new运算符

引用

引用做函数返回值

常量引用

函数的默认参数

函数重载

面向对象编程(OOP)

对象

抽象

封装

 类:

继承

多态

C++基础

C++中的文件组织

​编辑

 预处理指令     

头文件的可嵌套导致了数据类型的重复定义问题! 

const的应用

输入及输出

C++中的函数

C++11中的新特性

类和对象

对象 

对象的动态建立和释放

构造函数

构造函数的重载

带默认参数的构造函数

拷贝构造函数

析构函数 

析构函数:

何时调用析构函数:

对象数组和对象指针

友元

继承和派生

类的组合

多态

纯虚函数与抽象类

运算符重载

模板 

函数模板

类模板

类模板与友元

文章内容来源


绪论

内存模型

内存分区模型

C++程序在执行时,将内存大方向划分为4个区域:
代码区:存放函数体的二进制代码,由操作系统进行管理的

全局区:存放全局变量和静态变量以及常量。

栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。

堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程 

代码区:

存放 CPU 执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

全局区:

全局变量和静态变量存放在此.
全局区还包含了常量区,字符串常量和其他常量也存放在此.

该区域的数据在程序结束后由操作系统释放

全局变量

静态变量:在普通变量前添加 static 

常量:

        字符串常量

        const修饰的全局变量

栈区

由编译器自动分配释放,存放函数的参数值,局部变量

注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放 

*不要返回局部变量的地址

堆区

由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

在C++中主要利用new在堆区开辟内存 

//利用new关键字 可以将数据开辟到堆区

//指针 本质也是局部变量,放在栈上,指针保存的数据是放在堆区

引用

作用:给变量起别名

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

int a=10;

int &b=a;

注意事项
1、引用必须初始化

2、引用一旦初始化后,就不可以更改

void  fun1(){     int   a = 100, b=20;     int &r = a; //正确      int &r = b;//错误 }

b=a        //这是赋值操作,不是更改引用

3、只能为变量声明引用,不能为常文本(常数)声明引用。

void  fun1(){     int   a = 100 ;     int &r = a; //正确      int &b = 10;//错误 }

 引用做函数参数,形参会修饰实参;

引用做函数返回值

不要返回局部变量的引用

int& test01()
{
    int a= 10;//局部变量存放在四区中的栈区
    return a;
}
int main()
{
    int &ref = test01():
    cout<<“ref =”<<ref<< endl;//第一次结果正确,是因为编译器做了保留
    cout<<"ref =<<ref <<end1;//第二次结果错误,因为a的内存已经释放
    system( pause");
}

 函数的调用可以作为左值

int& test02{
    static int a= 10://静态变量,存放在全局区,全局区上的数据在程序结束后系统释放
    return a;
}
int main(){
    int &ref2 = test02():
    cout<<"ref2 ="<<ref2<<endl;
    test02()=1000;
    cout<<"ref2 ="<<ref2<<endl;
}

引用的本质:在C++内部实现指针常量 

常量引用

用来修饰形参,防止误操作。const

const int &v;

必须进行初始化,不能被更新。

面向对象编程(OOP)

特征:抽象、多态、封装、继承

 

1、对象

对象: 由一组属性和一组行为构成。

属性(数据):用来描述对象静态特征的数据项。

行为(函数):用来描述对象动态特征的操作序列。 

2、抽象

抽象:指从同类型的众多事物中舍弃个别的、非本质的属性和行为,而抽取出共同的、本质的属性和行为的过程。

3、封装

封装的意义:将属性和行为作为一个整体,表现生活中的事物。

将属性和行为加以权限控制
在设计类的时候,属性和行为写在一起,表现事物

语法:class 类名{访问权限:属性 /行为 };

 4、类

面向对象方法中的“类”是一种自定义的数据类型。

为属于该类的全部对象提供了抽象的描述,包括属性和行为两部分。

类 不仅是把函数放在什么地方的问题。还提供了:继承和多态等面向对象特性,为解决大型软件的复杂性和可重用性提供了有效的途径。

5、继承

继承的定义:子类的对象拥有其父类的全部属性与行为。

6、多态

多态是指不同的对象对相同的消息有不同的解释。 

例如:开车->开拖拉机、开小轿车、开大卡车、开火车、开摩托车、开飞机等都是“开”,但对象不同,具体动作也不同。

C++基础

C++中的文件组织

如果想将头文件和实现文件合并在一起,可以将文件名定义为“.hpp”。

 预处理指令     

  由编译器支持,不是C++语言部分     

  所有编译指令以 # 开头     

  每条指令单独占一行

作用:    

用于支持多文件形式组织的C++程序     

指示C++编译器将一个文件的内容嵌入到该指令所在位置

格式1:        

#include <文件名>     用于嵌入C++系统提供的头文件。   

 从C++系统目录的 INCLUDE 目录搜索指定文件

格式2:        

#include “文件名”     

用于嵌入程序员建立的头文件。     

首先搜索当前目录     

然后搜索C++系统目录的 INCLUDE 目录

头文件的可嵌套导致了数据类型的重复定义问题! 

若头文件中出现类或结构定义,则应在其外面施加保护  

保护的方法是采用宏定义指令.以保证该类或结构定义只执行一次

 


const的应用

const 常量 定义格式为:const 数据类型 常量名 = 常量值

const int MAX_SIZE =100;  //#define MAX_SIZE 100

float array[MAX_SIZE] ;

说明:

1、const 必须放在数据类型之前;

2、数据类型是一个可选项,如果省略了,则默认为int。建议在声明常量时,必须填写常量类型。  

3、const 的用途很多,可以修饰函数参数,防止被修改。

 int max(const int a, const int b);


输入及输出

 I / O 流是C++的标准流类,用于控制输出输入;

 I / O 流在 iostream.h 标准头文件。  

cout 对屏幕的标准输出流    << 向 cout流插入字符  

cin  对键盘的标准输入流 >> 从 cin 流抽取字符

#include <iostream>
using namespace std;
main()
{
   int  i, j ;
  cin >> i >> j ;      //从键盘输入i和j的值
  cout  << “i+j=” << i+j << ‘\n’ ;	  
                              //在显示器输出字符串和表达式的值
}		


C++中的函数

 函数调用及参数传递

按值传递:被调用函数本身不对实参进行操作,也就是说,即使形参的值在函数中发生了变化,实参的值也完全不会受到影响,仍为调用前的值。

地址传递:传递的内容:把实参的存储地址传送给对应的形参,从而使得形参指针和实参指针指向同一个地址。

按值传递             优点:函数调用简单,自然。        

                           缺点:参数“单向”传递。

按地址传递         优点:双向传递       

                           缺点:函数调用复杂

有没有一种方法能结合按值传递和按地址传递的优点,避免其缺点?

引用

//1、值传递
void mySwap0l(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;
}

内联函数

主要解决程序的运行效率问题

静态函数不可以定义成内联函数

内联函数中不能含有任何循环以及switch和goto语句;

内联函数中不能说明数组;

递归函数(自己调用自己的函数)不能定义为内联函数。

若函数体中有以上限制,即便函数前面标上inline ,该函数仍不为内联函数,被自动视为一般函数。

关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。

函数的默认参数

int func(int a,int b=10,intc=10)

{

        return a+b+c;

}

1、默认值的定义必须遵守从右到左的顺序,如果某个形参没有默认值,则它左边的参数就不能有默认值。

2、如果函数声明有默认值,函数实现的时候就不能有默认参数

int func2(int a = 10,int b =10);

int func2(int a, int b)

{
        return a + b;

}

函数重载

 作用:函数名可以相同,提高复用性
函数重载满足条件:
1、同一个作用域下
2、函数名称相同
3、函数参数类型不同 或者 个数不同 或者 顺序不同
注意:函数的返回值不可以作为函数重载的条件

类和对象

既然有结构体struct了,那么使用类class的好处是什么呢?C中struct的成员可以任意访问,对于一些隐私信息是不允许的。为了保护类中的数据安全,在C++中将类中的成员分为两类:公有成员(用public声明);私有成员(用private声明);protected.

私有成员(包括数据成员和成员函数)只能被类内的成员函数访问,不能被类外的对象访问。

公有成员(包括数据成员和成员函数)既可以被类内的成员函数访问,又可以被类外的对象访问。

实现类的外部接口

如果不添加pubic,类内的所有成员默认私有,而C的结构体默认公有。一般来说,把需要保护的数据设为私有,成员函数设为公有。成员函数为进入类的入口。

类的定义一般形式

class 类名{
    数据成员类型 数据成员列表
};

每个类可以没有成员,也可以有多个成员

类成员可以是数据或函数

所有成员必须在类的内部声明,一旦类定义完成后,没有其他方式添加成员。

每个类还可以包含成员函数,能够访问类自身的所有成员

类的数据成员

class Cube{
    long color;//数据成员
    double x,y,z,side;//数据成员
    int a[10];
    char *s;
    char &r;
    void *p;
}

内联成员函数

类的成员函数可以指定为inline,即内联函数。
默认情况下,在类体中定义的成员函数若不包括循环等控制结构符合内联函数要求时,C++会自动将它们作为内联函数处理(隐式inline)。

函数体直接定义在类体内,构成内联函数。


class Data { //Data类定义
    int getx(){ return x;} //内联成员函数
    inline int gety(){return y;} //显式指定内联成员函数
    inline void setxy(int_x,int_y)://显式指定内联成员函数
    void display();
    int x,y;
};
inline void Data::setxy(intx,int_y)//内联成员函数
{
    x=_x,y=_y;
}
void Data::display()//非内联成员函数
{
    ... //函数体
}

在类的外部定义成员函数 

class 类名{
    返回类型 函数名(类型1 参数名1,类型2 参数名2,...);
    //成员函数声明
    返回类型 函数名(类型1,类型2,...);
};
返回类型 类名::函数名(形式参数列表)
{
    函数体//访问类的数据成员
}

说明:(::)是作用域限定符

成员函数重载及默认参数

成员函数的存储方式

通常,C++会为每个对象的数据成员分配各自独立的存储空间,像结构体成员那样。

类的声明

class Point;//Point类声明,非Point类定义

 在创建类的对象之前,必须完整地定义该类。这样,编译器就会给类的对象准备相应的存储空间。
同样地,在使用引用或指针访问类的成员之前,必须已经定义类。
类不能具有自身类型的数据成员。然而,只要类名一经出现就可以认为该类己声明。因此,类的数据成员可以是指向自身类型的指针或引用

class Point;//Point类声明,非Point类定义,因为没有类体
class Line {
    Point a;//错误,不能使用仅有类声明而没有类定义的类定义数据对象
    Point *pp,&rp;//正确,只有类声明,即可用它定义该类的指针或引用
    Line b;//错误,类不能具有自身类型的数据成员
    Line*pl,&rl;//正确,类可以有指向自身类型的指针或引用的数据成员
};

对象 

对象是类的实例

对象的定义 

1、先定义类类型再定义对象

类名 对象名列表;

2、定义类类型的同时定义对象 

class 类名{ //类体

        成员列表

}对象名列表;

一般而言,定义类型时不分配存储空间,定义对象时将为其分配存储空间 

对象的动态建立和释放(new和delete)

利用new运算符可以动态地分配对象空间,delete运算符释放对象空间。

用new运算动态分配得到的对象是无名的,它返回对象的内存单元的起始地址。程序通过这个地址可以间接访问这个对象,因此需要定义一个指向类的对象的指针变量来存放该地址。
在执行new运算时,如果内存不足,无法开辟所需的内存空间C++编译器会返回一个0值指针。因此,只要检测返回值是否为0.就可以判断动态分配对象是否成功,只有指针有效时才能使用对象指针。

利用new创建的数据,会返回该数据对应类型的指针;

语法:new 数据类型;

指针变量名 = new 类型名(初始化式)

int *a=new int(10);        //创建一个数据

int *array=new int[10];        //开辟一个数组

当不再需要使用由new建立的动态对象时,必须用delete运算予以撤销。(析构函数)例如:

格式: delete <指针名>; 如 delete s;          

delete [ ] <指针名>;  如delete [ ]s;s为任意维数数组;

释放了p所指向的对象。此后程序不能再使用该对象,
注意,new建立的动态对象不会自动被撤销,即使程序运行结束也是如此,必须人为使用delete撤销

访问对象中的成员可以有3种方法


1、通过对象名和对象成员引用运算符(.)访问对象中的成员;

对象名.成员

对象名.成员函数(实参列表)

从类外部只能访问公有成员

2、通过指向对象的指针和指针成员引用运算符(->)访问对象中的成员;

对象指针->成员

对象指针->成员函数(实参列表)

3、通过对象的引用变量和对象成员引用运算符(.)访问对象中的成员;

对象引用变量名.成员

对象引用变量名.成员函数(实参列表)

对象赋值(同类)

对象名1=对象名2;

构造函数

创建一个类,默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默计析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数

创建对象时自动被调用(ctor)

在创建对象时初始化对象,为对象数据成员赋初始值

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

类的数据成员不能在类定义时初始化(原因:类定义时并没有产生一个实体,而是建立一种数据模型,不占用内存空间)

如果一个类中所有数据成员都是共有的,则可以在定义对象时对数据成员初始化

构造函数的定义

C++规定构造函数的名字与类的名字相同,并且不能指定返回类型。定义形式为:

类名(形式参数列表)

{

        函数体

}

构造函数可以没有形参,有如下两种形式:

类名()

{

        函数体

}

类名(void)

{
        函数体

}

关于构造函数的说明:
(1)构造函数是在创建对象时自动执行的,而且只执行一次,并先于其他成员函数执行。构造函数不需要人为调用,也不能被人为调用。
(2)构造函数一般声明为公有的(public),因为创建对象通常是在类的外部进行的。如果构造函数声明为保护的(protected)或私有的(protected),那就意味着在类外部创建对象(并调用构造函数)是错误的。换言之,这样的类是不能由外部实例化,只能由类内部实例化,这种情况不是通常的做法。

(3)在构造函数的函数体中不仅可以对数据成员初始化,而且可以包含任意其他功能的语句,例如分配动态内存等,但是一般不提倡在构造函数中加入与初始化无关的内容
(4)每个构造函数应该为每个数据成员提供初始化。否则将使那些数据成员处于未定义的状态。而使用一个未定义的成员是错误的
(5)带参数的构造函数中的形参,是在定义对象时由对应的实参给定的,用这种方法可以方便地实现对不同对象进行不同的初始化,需要注意,实参必须与构造函数的形参的个数、次序、类型一致。

声明指针并不会调用构造函数,只有分配空间的时候才会。

构造函数的重载

在一个类中可以定义多个构造函数版本,即构造函数允许被重载只要每个构造函数的形参列表是唯一的。一个类的构造函数数量是没有限制的。一般地,不同的构造函数允许建立对象时用不同的方式来初始化数据成员。

尽管在一个类中可以包含多个构造函数,但是对于每一个对象来说建立对象时只执行其中一个,并非每个构造函数都被执行。

带默认参数的构造函数

1、必须在类内指定默认参数

2、如果构造函数的全部参数都指定了默认值,则在定义对象时可以给一个或几个实参,也可以不给出实参。这时,就与无参数的构造函数有歧义了。

3、在一个类中定义了带默认参数的构造函数后,不能再定义与之有冲突的重载构造函数。

拷贝构造函数

功能:用已知的对象来创建一个新的同类的对象。

特点:  1、函数名与类同名,该函数没有返回类型;  

        2、函数只有一个参数,且是对类对象的引用;                Time(Time &src);      

        3、每个类都必须有一个拷贝构造函数。

如果类中没有说明拷贝构造函数,则编译系统自动生成一个缺省拷贝构造函数。作为该类的公有成员。

当用类的一个对象去初始化该类的另一个对象时系统自动调用拷贝构造函数实现拷贝赋值。

深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作

当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指的对象进行复制。

void test01(){
        Person p1(18);
        cout<<“p1的年龄为:<<pl.m Age<< endl;
        Person p2(p1):
        cout<<“p2的年龄为:<<p2.m_Age<< endl;}

浅拷贝问题利用深拷贝解决

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。 

对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a = 100;
int b = a; 

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。

类名(const 类名 &参数)

#include<iostream>
using namespace std;
class CExample
{
private:
    int a;
public:
    //构造函数
    CExample(int b)
    {
        a=b;
        printf("constructor is called\n");
    }
    //拷贝构造函数
    CExample(const CExample & c)
    {
        a=c.a;
        printf("copy constructor is called\n");
    }
    //析构函数
    ~CExample()
    {
        cout<<"destructor is called\n";
    }
    void Show()
    {
        cout<<a<<endl;
    }
};
int main()
{
    CExample A(100);
    CExample B=A;
    B.Show(); 
    return 0;
}


程序运行结果如下:

constructor is called
copy constructor is called
100
destructor is called
destructor is called          

使用条件

析构函数 

析构函数:

当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统会自动执行析构函数。(dtor)析构函数往往用来做“清理善后的工作(例如在建立对象时用new开辟了一段内存空间,则在该对象消亡前应在析构函数中用delete释放这段存储空间)

C++规定析构函数的名字是类名的前面加一个波浪号(~)。其定义形式为:

~类名()

{
        函数体

}

析构函数不返回任何值,没有返回类型,也没有函数参数。由于没有函数参数,因此它不能被重载。换言之,一个类可以有多个构造函数,但是只能有一个析构函数,

何时调用析构函数:


(1)对象在程序运行超出其作用域时自动撤销,撤销时自动调用该对象的析构函数。如函数中的非静态局部对象
(2)如果用new运算动态地建立了一个对象,那么用delete运算释放该对象时,调用该对象的析构函数。

需要特别注意对它们的调用时间和在使用构造函数和析构函数时,调用次序。
构造函数和析构函数的调用很像一个栈的先进后出,调用析构函数的次序正好与调用构造函数的次序相反。最先被调用的构造函数其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。
可简述为:先构造的后析构,后构造的先析构。

调用次序

对象数组和对象指针

对象数组

将具有相同类类型的对象有序地集合在一起便构成了对象数组,以一维对象数组为例,其定义形式为:

类名 对象数组名[常量表达式];


一维对象数组有时也称为对象向量,它的每个元素都是相同类类型的对象。

例如表示平面上若干个点,可以这样定义:
Point points[100];//表示100个点

关于对象数组的说明:
(1)在建立对象数组时,需要调用构造函数。如果对象数组有100个元素,就需要调用100次构造函数。
(2)如果对象数组所属类有带参数的构造函数时,可用初始化列表按顺序调用构造函数,使用复制初始化来初始化每个数组元素。(3)如果对象数组所属类有单个参数的构造函数时,定义数组时可以直接在初值列表中提供实参。
Point P[3]={Point(1,2),Point(5,6),Point(7,8)};//三个实参Student S[5]={20,21,19,20,19};//Student类只有一个数据成员

(4)对象数组创建时若没有初始化,则其所属类要么有合成默认构造函数(此时无其他的构造函数),要么定义无参数的构造函数或全部参数为默认参数的构造函数(此时编译器不再合成默认构造函数)
(5)对象数组的初始化式究竟是什么形式,,本质上取决于所属类的构造函数。因此,需要明晰初始化实参与构造函数形参的对应关系,避免出现歧义性。
(6)如果对象数组所属类含有析构函数,那末每当建立对象数组时,按每个元素的排列顺序调用构造函数;每当撤销数组时,按相反的顺序调用析构函数。

对象指针

在建立对象时,编译器会为每一个对象分配一定的存储空间,以存放其成员。对象内存单元的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的指针。指向类对象的指针变量的定义形式为:

类名 *对象指针变量名 =初值

成员指针和this指针 

C++比C语言有更严格的静态类型,更加强调类型安全和编译时检查。
因此,C++的指针被分成数据指针、函数指针、数据成员指针、成员函数指针四种,而且不能随便相互转换。其中前两种是C语言的,称为普通指针(ordinary pointer);后两种是C++专门为类扩展的,称为成员指针(pointerto member)
成员指针与类的类型和成员的类型相关,它只应用于类的非静态成员。由于静态类成员不是任何对象的组成部分,所以静态成员指针可用普通指针。 

什么时候会用到this指针
(1)在类的非静态成员函数中返回类对象本身的时候,直接使用return *this ;
(2)当参数与数据成员名相同时,如this->n=n(不能写成n=n)

this指针的const限定
假设Point类有getX这样一个非static函数:
double Point::getX();
编译以后形式如下:
double getX(Point *const this);
可以看出,this指针的指向不允许改变,所以this指针原本就是const指针。

同变量、函数一样,类也有自己的作用域。

(1)每个类都定义了自己的作用域和唯一的类型。在类体内声明类成员,将成员名引入类的作用域中。两个不同的类具有两个独立的类作用域。即使两个类具有完全相同的成员列表,它们也是不后的类型。每个类的成员不同于任何其他类的成员。

(2)在类作用域之外,成员只能通过对象、指针或引用的方式(使用成员访问操作符"“或"->")来访问。这些运算符左边的运算对象分别是一个类对象、指向类对象的指针或对象的引用,后面的成员名字必须在相对应的类的作用域中声明。

(3)静态成员、类中定义的类型成员需要直接通过类作用域运算符":“来访问。

 (4)定义于类外部的成员函数的形参列表和函数体也都是在类作用域中,所以可以直接引用类的其他成员。

类的结构

作用域、可见性、生存周期

作用域讨论的是标识符在程序中的有效范围,是一个标识符在程序正文中有效的区域。

1、块作用域(局部作用域) :当标记符的声明出现在由一对花括号所括起来的程序块内时,该标记符的作用域从声明点开始,到块结束处为止。

2、类作用域:指类定义及其的成员函数定义所涵盖的范围。 一个类的所有成员位于这个类的作用域内; 一个类的任何成员都能访问同一类的任一其它成员。

3、文件作用域(全局作用域): 是在所有函数或类定义之外说明的,其作用域从说明点开始,一直延伸到源文件结束。

可见性是标识符是否可以引用的问题。

标识符要声明在前,引用在后。

在同一作用域中,不能声明同名的标识符。

在没有互相包含关系的不同作用域中声明的同名标识符互不影响。

若在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见。

 对象从产生到结束的这段时间就是它的 生存期。

静态变量和静态成员

静态数据成员: 将类中数据成员说明为 static 。

静态数据成员为类的所有对象共享,而不是某个对象的成员。因为,静态成员只存储一处(全局数据区),供所有对象使用,一个对象对静态成员进行了改变后,其它同类对象再访问静态成员时是访问的改变后的值。

静态成员函数不是对象成员 没有this指针, 不能直接引用类的非静态成员,但可以引用静态数据成员。 

友元

友元是C++提供的一种破坏数据封装和数据隐藏的机制。

通过声明友元可以访问类中的私有或保护成员。

友元包括:友元函数和友元类。

使用友元的原因:减少系统开销。

为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。

若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。 声明语法:将友元类名在另一个类中使用friend修饰说明。

说明:友元是单向的 如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。 

运算符重载

运算符重载

运算符重载就是对现有的运算符重新进行定义,赋予其另一种功能以适应不同的数据类型。

对运算符重载,我们需要坚持四项基本原则: (1)不可臆造运算符; (2)运算符原有操作数的个数、优先级和结合性不能改变; (3)操作数中至少一个是自定义类型; (4)保持重载运算符的自然含义。

所谓重载,就是重新赋予新的含义。例如函数重载,就是对一个已有的函数赋子新的功能。
C++语言本身就重载了很多运算符,例如<<是位运算中的左移运算符,但在输出操作中又是与流对象cout配合是的流插入运算符>>运算符也是。
C++允许程序员重载大部分运算符,使运算符符合所在的上下文环境。虽然重载运算符的任务也可以通过显式的函数调用来完成,但是使用运算符重载往往使程序更清晰。

本质上,运算符重载是函数的重载。重载运算符使具有特殊名称的函数,形式如下:

返回类型 operator 运算符号(形式参数列表)

{

函数体

}

operator后接需要重载的运算符,成为运算符函数。
运算符函数的函数名就是“operator运算符号”。

重载运算符的规则
(1)C++绝大部分的运算符可以重载,不能重载的运算符有

.(成员引用).*(对象成员指针)::(域运算)?:(条件)sizeof(取长度)

只能重载C++语言中已有的、可以重载的运算符,不可臆造新的运算符。
(2)不能改变运算符的优先级、结合型和运算对象数目。

(3)运算符重载函数不能使用默认参数。
(4)重载运算符必须具有一个类对象(或类对象的引用)的参数,不能全部是C++的内置数据类型。
(5)一般若运算结果作为左值则返回类型为引用类型;若运算结果要作为右值,则返回对象。
(6)重载运算符的功能应该与原来的功能一致。

(7)不能改变操作数个数。

将运算符重载为类的成员函数,一般形式为:

class 类名{ //类体
返回类型 operator 运算符号(形式参数列表)

        {
                函数体

        }

}

或者:
class 类名{//类体
        返回类型 operator 运算符号(形式参数列表);
};
返回类型 类名::operator 运算符号(形式参数列表)

{
        函数体

}

前置单目运算符重载为类的成员函数,形式如下:

返回类型 类名::operator op()

{
        //this指针对应obi运算对象

}

经过重载后,表达式“op obj相当于obj.operator op()

后置单目运算符重载为类的成员函数,形式如下:

返回类型 类名::operator op(int)
{
        //this指针对应obi运算对象

}

经过重载后,表达式“obj op'相当于obj.operator op(0)

运算符重载时,运算符函数具有两种方式:       类的成员函数

                                                                            友员函数

这两种方式非常相似,关键区别在于     成员函数具有 this 指针  

                                                               友员函数没有 this 指针

不管用哪种形式重载运算符,对运算符的使用方法(调用)都是相同的。

当运算符重载为友元函数时,运算符函数的形式参数的个数和运算符规定的运算对象个数一致。
形式如下:

class 类名{ //类体
        //友元声明

        friend 返回类型 operator 运算符号(形式参数列表);

};

返回类型 operator 运算符号(形式参数列表)

{
        函数体

}

继承与组合

在多继承时,基类与派生类之间,或基类之间出现同名成员时,将出现访问时的( 二义性(或不确定性)),例如下面例子中的“c1.f()”就有这个问题。若想正确调用“c1.f()”,可在调用时利用类名通过类的作用域符“::”来限定f()来自哪个基类或在派生类中利用( 正确答案: 同名隐藏)规则声明一个同名的成员函数f()来解决。

在C++中,继承就是在一个已存在的类的基础上建立一个新的类已存在的类称为基类(base class),又称为父类;新建立的类称为派生类(derived class),又称为子类。
一个新类从已有的类那里获得其特性,这种现象称为类的继承
另一方面,从已有的父类产生一个新的子类,称为类的派生。派生类继承了基类的所有数据成员和成员函数,具有基类的特性,派生类还可以对成员作必要的增加或调整,定义自己的新特性。

继承方式

定义派生类的一般形式为:

class 派生类名:类派生列表{//类体

        成员列表

};

除了增加类派生列表外,派生类的定义与类定义并无区别。类派生列表(class derivation list)指定了一个或多个基类(baseclass),具有如下形式:
访问标号 基类名

 

赋值兼容

赋值兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决
赋值兼容规则中所指的替代包括以下的情况
①派生类的对象可以赋值给基类对象;
②)派生类的对象可以初始化基类的引用;
③派生类对象的地址可以赋给指向基类的指针

派生类构造函数

基类和派生类都需要声明自己的构造函数。

声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,自动调用基类的默认构造函数完成。

派生类的构造函数需要给基类的构造函数传递参数

派生类构造函数的定义
在执行派生类的构造函数时,使派生类的数据成员和基类的数据成员同时都被初始化。其定义形式如下:

派生类名(形式参数列表):基类名(基类构造函数实参列表),派生类初始化列表
{

        派生类初始化函数体

};

“基类名(基类构造函数实参列表)“即是调用基类构造函数,而派生类新增加的数据成员可以在“派生类初始化列表”(尽量在此)初始化,也可以在“派生类初始化函数体“中初始化。

派生类构造函数的调用顺序是:
①调用基类构造函数;②执行派生类初始化列表③执行派生类初始化函数体

类的保护成员用protected访问标号声明,可以认为protected访问标号是private和public的混合
①像私有成员一样,保护成员不能被类用户访问:②像公有成员一样,保护成员可以被该类的派生类访问。
如果基类声明了私有成员,那么任何派生类都是不能访问它们的若希望在派生类中能访问它们,应当把它们声明为保护成员。所以如果在一个类中声明了保护成员,就意味着该类可能要用作基类,在它的派生类中会访问这些成员。

 不同的继承方式决定了基类成员在派生类中的访问属性。
(1)公有继承(publicinheritance
基类的公有成员和保护成员在派生类中保持原有访问属性,私有成员仍为基类私有。
(2)私有继承(private inheritance.
基类的所有成员在派生类中为私有成员。
(3)保护继承(protected inheritance)
基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍为基类私有。

虚基类

虚基类的定义
虚基类是在派生类定义时,指定继承方式时声明的。声明虚基类的一般形式为:

class 派生类名: virtual 访问标号 虚基类名,...{ //类体

        成员列表

}


需要注意,为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。

类的组合

即在一个类中包含另一个类的对象

类组合的构造函数设计

  • 原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。
  • 声明形式:

类名(对象成员所需的形参,本类成员形参):对象1(参数),对象2(参数),...... { //函数体其他语句 }

如果写一个类,希望它能被经常被复用,那么要记住,不管写了多少个构造函数,都要写一个不带参数的默认构函数,因为当这个类的对象用在其他类的部件成员时,可能那个组合类根本就不写构造函数,就用默认构造函数。如果组合类不写构造函数,就没办法给部件对象传参数,这个时候就需要部件对象的一定得有默认构造函数。 

构造组合类对象时的初始化次序

  • 首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序。
    • 成员对象构造函数调用顺序:按对象成员的声明顺序,先声明者先构造。
    • 初始化列表中未出现的成员对象,调用用默认构造函数(即无形参的)初始化
  • 处理完初始化列表之后,再执行构造函数的函数体。

多态与抽象

不同对象调用相同名称的函数

派生一个类的原因并非总是为了继承或添加新成员,有时是为了重新定义基类的成员,使基类成员“获得新生”。
面向对象程序设计的真正力量不仅仅是继承,而是允许派生类对象像基类对象一样处理,其核心机制就是多态和动态联编。

多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。
从广义上说,多态性是指一段程序能够处理多种类型对象的能力。在C++中,这种多态性可以通过重载多态(函数和运算符重载)强制多态(类型强制转换)、类型参数化多态(板)、包含多态(继承及虚函数)四种形式来实现。

分两类:动态多态(派生类和虚函数运行时多态)和静态多态(函数重载、运算符重载)

动态多态满足条件:

继承关系

子类重写父类中的虚函数。重写(返回值类型,函数名,参数列表完全相同)。virtual关键字

动态多态使用:

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

多态的优点:

代码组织结构清晰;

可读性强;

利于前期后期维护;

例:实现计算器抽象类

class Abstractcalculator{
public:
    virtual int getResult(){
        return e;
    }
    int m_Num1;
    int m_Num2;
};

//加法计算器
class Addcalculator :public Abstractcalculator
{
    public:
        int getResult(){
            return m_Num1 + m_Num2;
        }
};
//减法计算器
class Subcalculator :public Abstractcalculator
{
    public:
        int getResult(){
            return m_Num1 - m_Num2;
        }
};

void teste2(){
//创建加法计算器
    AbstractCalculator *abc = new AddCalculator;
    abc->m Num1 =10;abc->m Num2 =10;
    cout << abc->m Num1 <<"+"<< abc->m um2<<"="<< abc->getResult()<< endl;delete abc; 
    //用究了记得销毁
    //创建减法计算器
    abc = new Subcalculator;
    abc->m_Num1.10;
    abc->m_Num2.10;
    cout << abc->m_lum1 <<"."<<abc->m_Num2<<"""<< abc->getResult()<< endl;
    delete abc;
}
int main(){
    teste2();
    system("pause");
    return 0;
}

静态联编

不同类对象中的同名函数(同样的消息)调用,采用“静态联编”。

联编(又叫绑定,binding) 编译器将函数名转换为唯一符号的过程,以便在程序中正确地链接函数。

静态绑定是指绑定工作出现在编译连接阶段。

函数重载、运算符重载,成员函数隐藏等都是静态绑定。

动态联编

动态联编(绑定) 是指在程序运行时进行的绑定工作。

实际上是在运行时,自动识别对象的类型,并确定要调用的确切的函数。是实现多态性的本质要求。

虚函数

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。

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

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

抽象类特点:

不能分配空间
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类

private 成员不能声明为虚函数。

只有类的成员函数才可以说明为虚函数(因为虚函数仅适、用于有继承关系的类对象)。

静态成员函数不能是虚函数(因为静态函数不受限于某个对象)。

内联函数不能是虚函数。因为内联函数是不能在运行中动态确定其位置的,如使虚函数在类内部实现,内联函数将失去其内联性。

构造函数不能是虚函数,因为构造时,对象还没有确定空间。 析构函数通常声明为虚函数,主要原因是防止内存无法释放。

虚函数具有继承性,基类中定义了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。 同原型函数指:<函数名、参数表和返回类型都完全相同>

本质:虚函数是覆盖定义,而非重载。

什么是static静态成员函数?

静态成员函数不属于类中的任何一个对象和实例,属于类共有的一个函数。也就是说,它不能用this指针来访问,因为this指针指向的是每一个对象和实例。

对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。

所以说,static静态函数没有this指针,也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。

例:制作饮品

制作饮品的大致流程为:煮水-冲泡-倒入杯中-加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

class AbstractDrinking {
public:
	virtual void Boil() = 0;
	virtual void Brew() = 0;
	virtual void PourInCup() = 0;
	virtual void Putsomthing() = 0;
	void makeDrink()
	{
		Boil();
		Brew();
		PourInCup();
		Putsomthing();
	}
};
class Coffee:public AbstractDrinking{
public:
	virtual void Boil(){
		cout<<"煮水"<<endl;
	}
	virtual void Brew(){
		cout<<"冲泡咖啡"<<endl; 
	} 
	virtual void PourInCup(){
		cout<<"倒入杯中"<<endl;
	}
	virtual void Putsomthing(){
		cout<<"加入糖和牛奶"<<endl; 
	}
};
class Tea:public AbstractDrinking{
public:
	virtual void Boil(){
		cout<<"煮矿泉水"<<endl;
	}
	virtual void Brew(){
		cout<<"冲泡茶水"<<endl; 
	} 
	virtual void PourInCup(){
		cout<<"倒入杯中"<<endl;
	}
	virtual void Putsomthing(){
		cout<<"加入枸杞"<<endl; 
	}
};

void dowork(AbstractDrinking *abs){
	abs->makeDrink();
} 
void test01(){
	dowork(new Coffee);
}

int main(){
	test01();
}

虚析构和纯虚析构共性:
可以解决父类指针释放子类对象
都需要有具体的函数实现


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

(1)重载多态
重载是多态性的最简单形式,分为函数重载和运算符重载
重定义已有的函数称为函数重载。在C++中既允许重载一般函数,也允许重载类的成员函数。如对构造函数进行重载定义,可使程序有几种不同的途径对类对象进行初始化。
C++允许为类重定义已有运算符的语义,使系统预定义的运算符可操作于类对象。如流插入(<<)运算符和流提取(>>)运算符(原先语义是位移运算)

设置虚基类的目的是 消除二义性

模板 

泛型编程主要应用的技术---模板

C++提供两种模板机制:函数模板和类模板

函数模板

函数模板作用:
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
语法:

template<typename T>
函数声明或定义

解释:

template --- 声明创建模板
typename --- 表面其后面的符号是一种数据类型,可以用class代替

T --- 通用的数据类型,名称可以替换,通常为大写字母

//交换整型函数

void swapInt(int& a, int& b){
        int temp = a;
        a = b;
        b= temp;

}

//交换浮点型函数


void swapDouble(double& a, double& b){

        double temp =a;
        a=b;
        b= temp;

}

//利用模板提供通用的交换函数

template<typename T>
void myswap(T& a, T& b){
        T temp = a;
        a= b;
        b = temp; 

}

void teste1(){
        int a = 10;
        int b = 20;
        //swapInt(a, b);
        //利用模板实现交换

                //1、自动类型推导

                        myswap(a, b);
                //2、显示指定类型

                        mySwap<int>(a,b);
"<< a<< endl;cout <<.3cout <<<< b<< endl; 

注意事项:

1、自动类型推导,必须推导出一致的数据类型T,才可以使用
2、模板必须要确定出T的数据类型,才可以使用 

typename可以替换成class;

例:数组排序

/*
【问题描述】采用模板技术定义选择排序函数模板SortArray

【输入形式】一系列数字
【输出形式】排序后的数字
【样例输入】3 2 1 4 5 6 7 8 9 10
【样例输出】1 2 3 4 5 6 7 8 9 10
*/

#include<iostream>  
using  namespace  std;
const  int  MAX_SIZE  =  10;
//函数名称为SortArray
template<typename T>
void SortArray(T a[],int MAX_SIZE)
{
        T temp;
		for(int i=0;i<MAX_SIZE;i++){
			T min=i;//认定最大值下标 
			for(int j=i+1;j<MAX_SIZE;j++)
			{
				if(a[min]>a[j])
				{
					min=j;//更新最小值下表 
				}
			}
			if(min!=i){
				//交换min和i元素
				temp=a[i];
				a[i]=a[min];
				a[min]=temp; 
			} 
	}

}

int  main()
{
        int    ar[MAX_SIZE];
        for  (int  i  =  0;  i  <  MAX_SIZE;  i++)
                cin  >>  ar[i];
      
        SortArray<int>(ar,  MAX_SIZE);
        cout  <<  ar[0];
        for  (int  i  =  1;  i  <  MAX_SIZE;  i++)
                cout  <<  "  "  <<  ar[i];
        cout  <<  endl;
        return  0;
}

普通函数与函数模板的区别:

1、普通函数调用可以发生隐式类型转换

2、函数模板 用自动类型推导,不可以发生隐式类型转换

3、函数模板 用显示指定类型,可以发生隐式类型转换 

普通函数与函数模板调用规则:

1.如果函数模板和普通函数都可以实现,优先调用普通函数                                                            2.可以通过空模板参数列表来强制调用函数模板
3.函数模板也可以发生重载
4.如果函数模板可以产生更好的匹配,优先调用函数模板

模板的局限性

模板不是万能的,有些特点数据类型,需要用具体化方式特殊实现.

总结:
利用具体化的模板,可以解决自定义类型的通用化

学习模板并不是为了用模板,而是在STL能够运用系统提供的模板

类模板

类模板的使用实际上是将类模板实例化为一个类

类模板定义

类模板由模板说明和类说明构成

   模板说明同函数模板,如下:

         template    <类型形式参数表>

         类声明

类模板与函数模板区别

1.类模板没有自动类型推导的使用方式

//Person p("孙悟空”、1000);错误,无法用自动类型推导
Person<string,int>p("孙悟空”,1000);//正确,只能用显示指定类型
2.类模板在模板参数列表中可以有默认参数 

template<class NameType,class AgeType int>

Person<string>p("猪八规”,999);//类模板中的模板参致列表 可以指定默认参致

类模板中成员函数创建时机

类模板中成员函数在调用时才去创建

类模板对象做函数参数

类模板实例化出的对象,向函数传参的方式
一共有三种传入方式:
1.指定传入的类型 …直接显示对象的教据类型

void printPersonl(Person<string, int>&p){
        p. showPerson() ;}
void test01(){
        Person<string, int>p("孙悟空”,100);

        printPersonl(p);}


2.参数模板化… 将对象中的参数变为模板进行传递

template<class T1,class T2>

void printPerson2(Person<T1,T2>&p){
        p. showPerson ();}
void test02(){
        Person<string,int>p("猪八戒”,90);

        printPerson2(p);}

3.整个类模板化… 将这个对象类型 模板化进行传递

template<class T>

void printPerson3(T &p){
        p.showPerson(){cout<<“T的数据类型为:"<< typeid(T).name ()<< endl;}
void test03(){
        Person<string, int>p("唐僧”,30);

        printPerson3(p);}

template<class Tl,class T2>
class Person{
    public:
        Person(Tl name, T2 age);
        this->m Name = name ;
        this-m Age = age;
        void showPerson()    {
            cout<<“姓名: "<< this->m Name<< ” 年龄:"<< this->m Age<< endl;}
        T1 m_Name ;
        T2 m_Age;

类模板与继承

//class Son :public Base //错误,必须要知道父类中的T类型,才能继承给子类

class Son:public Base<int>{}
void test01()
{Son sl;}

类模板成员函数类外实现

//构造函数类外实现

template<class Tl,class T2>

Person<T1, T2>::Person(Tl name, T2 age){
this->m_Name = name ;this->m_Age = age; }

类模板与友元

文章内容来源

08 模板-类模板基本语法_哔哩哔哩_bilibili

C++程序设计_中国大学MOOC(慕课) (icourse163.org)

一文看懂C++类的拷贝构造函数所有用法(超详细!!!)-CSDN博客

C++类的组合(学习笔记:第4章 06) - 知乎 (zhihu.com)

静态成员函数为什么不能是虚函数_虚函数可以是静态成员函数吗-CSDN博客

C++ 类模板(template)详解_c++模板类-CSDN博客

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值