学习笔记 dayAll C++

一 命名空间

/*
在c语言中,使用static关键字来解决命名冲突问题
在c++中,使用namespace来解决
*/
#include <iostream>
namespace myspaceA
{
    int a = 5;
}
namespace myspaceB
{
    int a = 10;
}
using namespace myspaceA;//尽量少用using导入命名空间,可能导致命名冲突
using namespace std;//此命名空间存放的是c++相对c升级的功能(函数、库文件、变量)
    
/*
以下情况:
    不用源文件可以有相同的命名空间;
    使用命名空间加作用域限定符访问成员时,优先导入本文件的命名空间;
    使用using导入命名空间时,导入的是所有程序中的命名空间;(只要没有命名冲突,都是被允许的)直接导入
*/
//命名空间应该定义在头文件中
int main(int argc, char const *argv[])
{
    //printf("a=%d\n", myspaceA::a);//a=5
    //printf("a=%d\n", myspaceB::a);//a=10
    printf("a=%d\n" a);//a=5
    return 0;
}

二 输入与输出

/*
c++的I/O发生在流中,流是字节序列
cout(标准输出流):预定义的对象cout是iostream类的一个实例
cin(标准输入流):预定义的对象cin是iostream类的一个实例
cerr(标准错误流):cerr是iostream类的一个实例;cerr对象是非缓冲的,且每个流插入到cerr都会立即输出
clog(标准日志流):clog是iostream类的一个实例;clog对象是缓冲的

cout与printf一致,存在缓冲区,数据满一行输出。
endl的作用就是在缓冲区中写入换行符

缓冲区垃圾的问题:getchar、cin.get()
在c++中使用cin.getline(s,100);来输入一行数据(包括空格)

*/
#include <iostream>
using namespace std;
int main(int argc, char const *argv[])
{
    cout << "hello world" << endl;
    cout << "hello world\n";
    return 0;
}

三 引用

/*

引用是为已存在的变量取了一个别名,引用和引用的变量共用同一块内存空间。
类型& 引用变量名 = 被引用变量名
int a=10;
int& b=a;
b是a的别名;b与a所占空间一样


解决了函数指针传参和返回值的问题
引用就是给变量起别名,操作引用相当于操作所绑定的变量
注意:定义引用必须绑定变量;
	绑定一个变量就不能再绑定其他变量

引用作为函数形参:传值和传地址问题
引用是否占用额外内存空间:占用  不将空间展示给开发人员

c++11:左值引用 右值引用
左值:可以被修改的值(可以取地址的)
右值:不可以被修改的值(不可以取地址的)
左值引用:只能绑定左值 int &
右值引用:只能绑定右值	int &&(对象引用)
std::move()//将左值转成右值


特点:
1 引用实体和引用类型必须为同种类型
2 引用在定义时必须初始化
3 一个实体可以有多个引用,但一个引用只能引用一个实体
4 引用只能初始化引用一个实体,而指针可以在任何时候指向任何一个同类型实体
5 没有NULL引用,但有NULL指针
6 在sizeof中含义不同:引用结果为引用类型的大小,指针始终是地址空间所占字节个数(32位:4字节   64位:8字节)
7 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
8 有多级指针,但是没有多级引用


引用和指针:
可以肯定的说,引用能实现的功能,指针都能实现。引用只是对指针的一个简单封装,作为函数参数时,达到数据双向传递和减少开销的目的。
对于参数传递,减少大对象的参数传递开销这两个用途来说,引用可以很好的代替指针,但有些时间,引用不能代替指针:
	如果一个指针所指向对象,需要用分支语句加以确定,或者在中途需要改变它所指向的对象,而引用只能在初始化时指定被引用的对象,所以不能胜任
	有时一个指针可能是空指针,因为没有空引用,所以不能胜任
	使用函数指针,由于没有函数引用,所以函数指针无法被代替
	用new动态创建的对象或数组,需要用指针来存储它的地址,此种情况也可以用引用:T &s = *(new T()); delete &s;
	以数据形式传递大量时,需要用指针来接收参数(参数表中出现T s[]和T* s是等价的)
*/
void swap(int &a, int &b) {
    int t = a;
    a = b;
    b = t;
}

void sort(int arr[], int len) {
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
                // int t = arr[j];
                // arr[j] = arr[j + 1];
                // arr[j + 1] = t;
            }
        }
    }
}

四 类与对象

/*
无论哪一种编程语言,其基本数据类型都是有限的,C++的基本数据类型也同样不足以表达现实世界中的各种对象。于是,C++语言提供了自定义数据类型的支持,这就是类。
类:用户自定义数据类型,抽象数据类型,复合数据类型。简言之,不能用一个基本数据类型描述的事物,都可以用类来表达。
对象:根据以上的类型,即类,创建出来的变量,在面向对象语言中,称为对象。
比如人,学生,手机,电脑,等等,这些都可以是类。而某一个具体的人,某几个具体的学生,具体的一台手机,电脑就是对象,可以说万物皆对象。
*/

1 面对对象的特点

抽象、封装、继承、多态简介

/*
抽象:对同一类事物,根据我们需要处理的细节,研究方向,对这类事物进行抽象,忽略不重要的特征,抽取重要的特征,这就是抽象。比如我们有一个学校学生信息管理的项目,需要统计学生的信息,因此要对学生进行抽象,可以抽象出哪几个特征呢?比如学号,姓名,性别,年龄,身份证号。而体重,身高可能就忽略掉了。而如果是要写一个征婚网站,则每个会员的信息就要包括身高,体重,甚至薪水。因此,这个抽象,对于不同的需求,抽象的细节也不同。

封装:封装就是将抽象得到的数据和行为相结合,形成一个有机的整体,也就是一个新的用户自定义的复合数据类型,可以被复用。C++语言中使用类来创建新的数据类型,完成封装。
类中包含函数成员和数据成员。一对{}限定了类的边界。关键字public和private是用来指定成员的不同访问权限的。声明为public的成员,外界可以访问。声明为private的成员是本类的私有数据,只有本类的函数可能访问,其它的类无法访问这些私有成员。封装使得一部分成员充当该类的外部接口,而将其它成员对外界隐藏起来,这样就达到了对成员访问权限的合理控制。使类之间的互相访问被严格控制,增加了数据的安全性,并且简化了程序的编写。
c++中,如果我们对类的成员(包括成员变量和成员函数)没有定义访问属性,则默认是private。

继承:C++语言提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体,更特殊的派生类的设计。
    优点:
    	代码可重用
    	方便扩展功能
    	父类的方法和属性可用于子类
    	设计简单

多态:多态性指一段程序能够处理多种类型对象的能力。在C++语言中,这种多态性可以通过强制多态,重载多态,类型参数化多态,包含多态4种形式来实现。
	强制多态是通过将一种类型转换成另一种类型的数据来实现的,要求在类型上有继承关系。
	重载是指给同一个名字赋予不同的含义,如函数重载和运算符重载
	虚函数实现包含多态,虚函数是多态的精华
	模板是实现参数化多态的工具,分为函数模板和类模板两种
*/

2 类 对象

/*
类是面向对象程序设计方法的核心,利用类可以实现对数据的抽象,创建新的数据类型。抽象,封装,继承和多态都是基于类的或者说表现在类上。
类是对逻辑上相关的函数与数据的封装,它是对问题的抽象描述。
类的实现有两种方式, 一种是在类定义时完成对成员函数的定义,另一种是在类定义的外部进行完成。

访问控制:
类的公有成员,是对外提供的接口。其它类可以访问。
类的私有成员,只能由类的接口来访问,不对外提供服务,因此,其它类只能通过调用公有接口方法来间接的访问私有成员。
访问控制属性有以下三种:公有,私有和保护类型。
保护成员在其子类中可以访问,但私有成员,子类访问不到。

public成员是公共成员,本类以及子类乃至类外都可以访问,是访问限制最少的成员。
protected 是保护成员,只允许在本类或者子类中才可以访问。类外是无法访问的。

一个没任何公有接口的类,是没有用的。设计一个类,就是为了使用它,要能够使用,就要设计必要的外部接口。
*/
#include <cstdio>
#include <iostream>
using namespace std;
/*
class Student {
public:
    void setAge(int age) {
        // this->age = age;
        if (age <= 0) {
            printf("age error!\n");
            this->age = 20;
        }
    }
    int getAge() {
        return age;
    }

private:
    int age;
    char name[10];
};*/
class Plural {
private:
    int r;
    int v;

public:
    void set(int r, int v) {
        this->r = r;
        this->v = v;
    }
    Plural plus(Plural c) {
        Plural a;
        a.r = c.r + this->r;
        a.v = c.v + this->v;
        return a;
    }
    Plural minus(Plural c) {
        Plural a;
        a.r = this->r - c.r;
        a.v = this->v - c.v;
        return a;
    }
    Plural mutiple(Plural c) {
        Plural a;
        a.r = (this->r * c.r) - (this->v * c.v);
        a.v = (this->r * c.v) + (this->v * c.r);
        return a;
    }
    void show() {
        if (this->r == 0) {
            printf("%di\n", this->v);
            return;
        }
        if (this->v == 0) {
            printf("%d\n", this->r);
            return;
        }
        if (this->v < 0) {
            printf("%d%di\n", this->r, this->v);
            return;
        }
        printf("%d+%di\n", this->r, this->v);
    }
};
int main(int argc, char const *argv[]) {
    // Student s;
    // s.setAge(0);
    // printf("age:%d\n", s.getAge());
    Plural p1;
    p1.set(-1, 2);
    Plural p2;
    p2.set(3, 4);

    p1.show();
    p2.show();
    Plural p3;

    p3 = p1.plus(p2);
    printf("+:");
    p3.show();

    p3 = p1.minus(p2);
    printf("-:");
    p3.show();

    p3 = p1.mutiple(p2);
    printf("*:");
    p3.show();

    return 0;
}

举例:Calendar


/*分文件编写*/
//calendar.h
#include "calendar.h"
#include <cstdio>
#include <iostream>

int main() {
    int year, month;
    printf("input year and month:\n");
    scanf("%d %d", &year, &month);
    Calendar c1(year, month);

    // c1.set(2023, 2);
    c1.printCalendar();
}

//calendar.cpp
#include "calendar.h"
Calendar::Calendar(int year, int month) {
    this->year = year;
    this->month = month;
}
void Calendar::set(int year, int month) {
    this->year = year;
    this->month = month;
}
void Calendar::printCalendar() {
    init();
    printf("====================%d年--%d月====================\n", year, month);
    printf("Sun\tMon\tTue\tWed\tTur\tFri\tSat\n");

    for (int i = 0; i < blanks; i++) {
        printf("\t");
    }
    for (int i = 1; i <= dayCount; i++) {
        printf("%d\t", i);
        if ((blanks + i) % 7 == 0) {
            printf("\n");
        }
    }
    putchar(10);
}

void Calendar::init() {
    for (int i = 1900; i < year; i++) {
        total += isRn(i) ? 366 : 365;
    }
    for (int i = 1; i <= month; i++) {
        switch (i) {
            case 4:
            case 6:
            case 9:
            case 11:
                dayCount = 30;
                break;
            case 2:
                dayCount = isRn(year) ? 29 : 28;
                break;
            default:
                dayCount = 31;
        }
        if (i < month) {
            total += dayCount;
        }
    }
    blanks = (total + 1) % 7;
}
bool Calendar::isRn(int year) {
    return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}

//main.cpp
#include "calendar.h"
#include <cstdio>
#include <iostream>

int main() {
    int year, month;
    printf("input year and month:\n");
    scanf("%d %d", &year, &month);
    Calendar c1(year, month);

    // c1.set(2023, 2);
    c1.printCalendar();
}

举例:Fraction

//fraction.h
#ifndef __FRACTION_H
#define __FRACTION_H

#include "stdio.h"

class Fraction {
private:
    int n, d;
    int GCD(int m, int n){
        if(n == 0){
            return m;
        }
        return GCD(n, m%n);
    }

    Fraction reduce(){
        int n = this->n / GCD(this->n, this->d);
        int d = this->d / GCD(this->n, this->d);
        return Fraction(n, d);
    }

public:
    Fraction(int n, int d){
        this->n = n;
        this->d = d;
    }

    Fraction plus (Fraction f){
        int n = this->n * f.d + this->d * f.n;
        int d = this->d * f.d;
        return Fraction(n, d);
    }
    Fraction minus (Fraction f){
        int n = this->n * f.d - this->d * f.n;
        int d = this->d * f.d;
        return Fraction(n, d);
    }
    Fraction multiple (Fraction f){
        int n = this->n * f.n;
        int d = this->d * f.d;
        return Fraction(n, d);
    }
    Fraction divide (Fraction f){
        int n = this->n * f.d;
        int d = this->d * f.n;
        return Fraction(n, d);
    }
    void show(){        
        printf("%d/%d\n", this->reduce().n, this->reduce().d);
    }
};


#endif


//main.c
#include "fraction.h"

int main(){
    Fraction f1(2, 5);
    Fraction f2(2, 6);

    Fraction f3 = f1.plus(f2);
    
    f3.show();

    Fraction f4(3, 7);
    f3.multiple(f4).show();
    f3.divide(f4).show();
}

/*output
11/15
11/35
77/45
*/

几种继承区别

/*
几种继承的区别:
在c++中,struct和class都是类的关键字,struct默认继承方式为公有继承,class默认继承方式为私有继承。
不管使用哪种继承方式,派生类内部都可访问在基类中的public或者protected成员,不同的继承方式继承后的权限会保持不变或者缩小,基类中的private成员在派生类中都是不可见的。

除了基类的构造函数,析构函数,私有成员之外,派生类继承了基类的全部成员。但是,这些成员的访问属性在派生过程中是可以调整的。

类的继承方式有:公有继承,私有继承,保护继承
a). 公有继承
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中保持不变,而基类的私有成员不可访问。
即基类的公有成员和保护成员被继承到派生类中仍作为派生类的公有和保护成员。

b). 私有继承       ((默认继承方式))
当类的继承方式为私有继承时,基类的公有和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。
即基类的公有成员和保护成员被继承到派生类中作为派生类的私有成员。

c). 保护继承
当类的继承方式为保护继承时,基类的公有和保护成员都以保护成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。
即基类的公有成员和保护成员被继承到派生类中作为派生类的保护成员。

而无论派生类的成员还是对象都无法访问基类的私有成员。
无论哪种继承方式,基类的公有和保护成员都可以被派生类的成员访问。
无论哪种继承方式,基类的私有成员都不可以被派生类的成员和对象访问。
*/
/*
public公有继承:基类的公有成员和属性成为派生类的公有;基类的被保护的属性和方法成为派生类的被保护;基类私有成员不能被继承。

private私有继承:基类的公有成员和属性成为派生类的私有;基类的被保护的属性和方法成为派生类的私有;基类私有成员不能被继承。

protected被保护继承:基类的公有成员和属性成为派生类的被保护;基类的被保护的属性和方法成为派生类的被保护;基类私有成员不能被继承。

*/

3 引用

/*

引用是为已存在的变量取了一个别名,引用和引用的变量共用同一块内存空间。
类型& 引用变量名 = 被引用变量名
int a=10;
int& b=a;
b是a的别名;b与a所占空间一样


解决了函数指针传参和返回值的问题
引用就是给变量起别名,操作引用相当于操作所绑定的变量
注意:定义引用必须绑定变量;
	绑定一个变量就不能再绑定其他变量

引用作为函数形参:传值和传地址问题
引用是否占用额外内存空间:占用  不将空间展示给开发人员

c++11:左值引用 右值引用
左值:可以被修改的值(可以取地址的)
右值:不可以被修改的值(不可以取地址的)
左值引用:只能绑定左值 int &
右值引用:只能绑定右值	int &&(对象引用)
std::move()//将左值转成右值


特点:
1 引用实体和引用类型必须为同种类型
2 引用在定义时必须初始化
3 一个实体可以有多个引用,但一个引用只能引用一个实体
4 引用只能初始化引用一个实体,而指针可以在任何时候指向任何一个同类型实体
5 没有NULL引用,但有NULL指针
6 在sizeof中含义不同:引用结果为引用类型的大小,指针始终是地址空间所占字节个数(32位:4字节   64位:8字节)
7 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
8 有多级指针,但是没有多级引用


引用和指针:
可以肯定的说,引用能实现的功能,指针都能实现。引用只是对指针的一个简单封装,作为函数参数时,达到数据双向传递和减少开销的目的。
对于参数传递,减少大对象的参数传递开销这两个用途来说,引用可以很好的代替指针,但有些时间,引用不能代替指针:
	如果一个指针所指向对象,需要用分支语句加以确定,或者在中途需要改变它所指向的对象,而引用只能在初始化时指定被引用的对象,所以不能胜任
	有时一个指针可能是空指针,因为没有空引用,所以不能胜任
	使用函数指针,由于没有函数引用,所以函数指针无法被代替
	用new动态创建的对象或数组,需要用指针来存储它的地址,此种情况也可以用引用:T &s = *(new T()); delete &s;
	以数据形式传递大量时,需要用指针来接收参数(参数表中出现T s[]和T* s是等价的)
*/
void swap(int &a, int &b) {
    int t = a;
    a = b;
    b = t;
}

void sort(int arr[], int len) {
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
                // int t = arr[j];
                // arr[j] = arr[j + 1];
                // arr[j + 1] = t;
            }
        }
    }
}

4 函数扩展

/*
内嵌函数:以空间换取时间inline
*/
/*
默认参数:给参数赋默认值
*/
/*
函数的参数占位符:预留函数接口
*/
/*
函数重载:函数命名问题	(函数形参的个数不同;形参个数相同、类型相同;个数相同、类型不同、顺序不同)
注意:函数返回值不能作为重载,函数默认参数会影响重载条件
*/

5 for与关键字

for

int a[5]={1,2,3,4,5};
for(int temp:a){
    cout<<temp<<endl;
}

关键字

/*
register:提高程序允许效率:省去CPU从内存抓取数据的开销
    语法作用:尽可能将变量保存在CPU内部寄存器中
    使用注意事项:只能修饰局部变量,不能修饰全局变量和函数
    			使用register修饰的变量不能通过取地址获取地址(有可能保存至cpu内部寄存器中)
    			register修饰的变量类型一定是cpu能处理的数据类型(某些cpu不支持浮点型)
    什么时候使用:频繁访问的变量
    优化的内容:当对其修饰的变量取地址时,c++会将该变量重新保存到内存中
volatile:防止编译器优化(将变量优化到寄存器中,寄存器存在边际效应)
    使用场景:访问硬件时所使用的全局变量
auto:自动变量,离开作用域自动释放
    优化内容:类型推导,根据所赋的值自动判断数据类型  auto num=5;其类型为int
typedef:给数据类型取别名(提高代码可读性、移植性)(重命名函数指针时可读性差)
    优化内容:使用using
const:将变量变为只读变量(修饰函数形参,保护实参在函数执行过程中不被改变)
    优化内容:const修饰的变量是常量
    constexpr:用来替代define,在预处理阶段处理,const在编译阶段处理。
bool:c++支持bool类型数据变量
三目运算符:在c++中可以做左值(结果作为左值)
while支持逗号表达式//
*/


    
/*
一维数组:char s[100]="hello world";
	s数组名:数组首元素地址	s+1-----1
	&s:数组的地址			&s+1----100
	*(&s)=s				对一维数组的地址取值等于数组首元素的地址
	
二维数组:char ktr[3][100]={"hello1","hello2","hello3"}
	ktr:二维数组的首个一维数组的地址
	&ktr:二维数组的地址
	*ktr:二维数组的首个一维数组的首元素地址
	**ktr:二维数组的首个一维数组的首元素的值
	*(&ktr)=ktr:对二维数组取值等于威威数组中首个一维数组的地址
	
指针数组:实际上还是一个一维数组,只不过里面保存的是指针
*/

五 构造函数

简介

/*
特点:没有返回值、函数名与类名相同、可以重载、当实例化对象时自动调用 
特点2:系统会默认生成构造函数
规则:当用户未给类定义任何构造函数时,系统会生成一个无参构造函数;如果定义了其他构造函数,系统不会生成
c++11:default delete
构造函数的分类:
	默认无参构造函数/自定义的有参构造函数;
	类型转换构造函数:构造函数只有一个参数;
	(如何解决类型转换函数带来的隐式转换风险:explicit:关闭编译器的隐式转换)(如何类型转换:重载类型转换运算符)
	this指针:保存当前对象的地址,对应的对象的方法形参都会多一个相应类型的this指针,传参时将对象的地址传递给this
	初始化列表(定义并初始化):
		处理特殊的成员属性:
			引用(定义必须初始化);
			const成员:定义必须初始化
			成员对象(无默认构造函数):实例对象时会自动调用构造函数(定义并初始化)
	初始化列表的效率高于在构造函数内部初始化,优先使用初始化列表
						
	拷贝构造函数:用已有的对象初始化新的对象;
		默认生成:当类里未定义拷贝构造函数,系统默认生成拷贝构造函数
		函数原型:Student(const Student& stu);将stu里面的成员赋值给当前对象
		**:默认生成一个等号运算符重载函数:拷贝赋值运算符重载
	移动拷贝构造函数:
		c++11提出对象移动语义
		对象移动:解决对象(临时对象)拷贝赋值开销的问题(拷贝的开销大)将原有对象的成员空间所有权传递给新的对象,不发生值的拷贝
		提出右值引用的目的:实现对象移动
*/
/*
c++中的构造函数分为构造函数和复制构造函数。构造函数可以有多个,而复制构造函数只能有一个,因为复制构造函数的参数只能是当前类的一个对象的引用,参数列表是固定的,无法重载,若用户没有定义自己的复制构造函数,系统会自动生成一个复制构造函数,其作用是将参数值赋予当前的对象.若用户自己定义了复制构造函数,系统则不会生成默认复制构造函数。

在程序执行过程中,当遇到对象声明语句时,程序会向操作系统申请一定的内存空间用于存储新建的对象。
程序员需要编写代码来初始化对象,C++中严格规定了初始化程序的接口形式,并有一套自动的调用机制。这里所说的初始化程序,便是构造函数。

构造函数的作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。
构造函数也是一个成员函数,队了具有一般成员函数的特点外,还有一些特殊的性质:
    构造函数名与类名相同,通常被声明为公有函数,无返回类型关键字。
    只要类中有构造函数,编译器就会在建立对象的地方自动插入对构造函数调用的代码。因此,通常构造函数会在对象创建时自动调用。
    如果类中没有写构造函数,编译器会自动生成一个默认的无参构造函数,其参数列表和函数体都为空。
    如果类中声明了构造函数,编译器就不再生成默认构造函数。


*/


/*
特点:没有返回值、函数名与类名相同、可以重载、当实例化对象时自动调用 
特点2:系统会默认生成构造函数
规则:当用户未给类定义任何构造函数时,系统会生成一个无参构造函数;如果定义了其他构造函数,系统不会生成
c++11:default delete
构造函数的分类:
	默认无参构造函数/自定义的有参构造函数;
	类型转换构造函数:构造函数只有一个参数;
	(如何解决类型转换函数带来的隐式转换风险:explicit:关闭编译器的隐式转换)(如何类型转换:重载类型转换运算符)
	this指针:保存当前对象的地址,对应的对象的方法形参都会多一个相应类型的this指针,传参时将对象的地址传递给this
	初始化列表(定义并初始化):
		处理特殊的成员属性:
			引用(定义必须初始化);
			const成员:定义必须初始化
			成员对象(无默认构造函数):实例对象时会自动调用构造函数(定义并初始化)
	初始化列表的效率高于在构造函数内部初始化,优先使用初始化列表
						
	拷贝构造函数:用已有的对象初始化新的对象;
		默认生成:当类里未定义拷贝构造函数,系统默认生成拷贝构造函数
		函数原型:Student(const Student& stu);将stu里面的成员赋值给当前对象
		**:默认生成一个等号运算符重载函数:拷贝赋值运算符重载
	移动拷贝构造函数:
		c++11提出对象移动语义
		对象移动:解决对象(临时对象)拷贝赋值开销的问题(拷贝的开销大)将原有对象的成员空间所有权传递给新的对象,不发生值的拷贝
		提出右值引用的目的:实现对象移动
*/

1 复制构造函数

/*
是和一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。其作用是使用一个已存在的对象(由参数指定),去初始化同类的一个新对象。
如果程序员没有定义类的复制构造函数,系统就会在必要时生成一个隐含的复制构造函数。

普通构造函数是在对象被创建时被调用,而复制构造函数在以下3种情况下都会被调用,以完成基本的对象复制工作。
1 Point b(a);  //用对象a初始化对象b,复制构造函数调用
  Point c = a;  //用对象a初始化对象c,复制构造函数调用(不再调用c的其它构造函数,一个对象的创建只会要调用一个构造函数)
  以上两种写法形式不同,实际上完全一样。
2 如果函数的形参是类的对象,调用函数时,复制构造函数调用。
  如果传递引用,则不会调用复制构造函数。由于这一原因,传递比较大的对象时,传递引用会比传递值效率高。
3 函数的返回值是一个类的对象。这种情况已经不调用复制构造方法了,被编译器优化掉了,vc++还调用。
4 构造方法后的初始化列表中初始化的成员是对象

当以上有一条发生,而被复制的对象中有指针,就带来严重bug的风险。
规避此风险的方法:
1) 自定义此对象的复制构造函数,让编译器默认的合成复制构造函数不起作用;
2) 函数参数的传入用指针或引用,不要使用对象;
3) 函数返回值采用指针和引用,不要使用对象;
*/

举例:Point类

#include <cstdio>
#include <iostream>
#include <math.h>

class Point {
public:
    Point() {}
    Point(int x, int y) {
        this->x = x;
        this->y = y;
    }
    Point(Point &p) {
        this->x = p.x;
        this->y = p.y;
        printf("there:copy structure function!\n");
    }
    ~Point() {
        printf("~Point  addr=%p has been destroyed!\n", this);
    }
    void show() {
        printf("Point[x=%d,y=%d]\n", x, y);
    }
    double distance(Point p1) {
        return sqrt((p1.x - this->x) * (p1.x - this->x) + (p1.y - this->y) * (p1.y - this->y));
    }

private:
    int x, y;
};

int main(int argc, char const *argv[]) {
    Point p(3, 2);
    Point p1(5, 6);
    p.show();
    p1.show();
    printf("dis:%lf\n", p1.distance(p));
    return 0;
}
/*运行结果:
Point[x=3,y=2]
Point[x=5,y=6]
there:copy structure function!
dis:4.472136
~Point  addr=0x7ffef7d9b660 has been destroyed!
~Point  addr=0x7ffef7d9b658 has been destroyed!
~Point  addr=0x7ffef7d9b650 has been destroyed!
*/
/*
首先进行两次自定义构造,然后一次拷贝构造,至此三次构造,结束时运行三次析构
*/

举例:Line类

//point.h
#ifndef _POINT_H_
#define _POINT_H_

#include <cstdio>
#include <iostream>
#include <math.h>

class Point {
public:
    Point() {}
    Point(int x, int y) {
        this->x = x;
        this->y = y;
    }
    Point(Point &p) {
        this->x = p.x;
        this->y = p.y;
        printf("there:copy structure function!\n");
    }
    ~Point() {
        printf("~Point  addr=%p has been destroyed!\n", this);
    }
    void show() {
        printf("Point[x=%d,y=%d]\n", x, y);
    }
    double distance(Point p1) {
        return sqrt((p1.x - this->x) * (p1.x - this->x) + (p1.y - this->y) * (p1.y - this->y));
    }

private:
    int x, y;
};

#endif

//line.h
#include "point.h"

class Line {
public:
    Line() {}
    Line(Point a, Point b) : start(a), end(b) {//此处设计参数列表
        // this->start = a;
        // this->end = b;
    }
    double length() {
        return start.distance(end);
    }

private:
    Point start;
    Point end;
};

//main.cpp
#include "line.h"
int main(int argc, char const *argv[]) {
    // Point a(3, 2);
    // Point b(5, 6);
    // Line l1(a, b);
    Line l(Point(3, 2), Point(5, 6));
    printf("length:%lf\n", l.length());
}

举例:CString类

//cstring.h
#ifndef _CSTRING_H_
#define _CSTRING_H_

#include <cstdio>
#include <cstring>
#include <iostream>
#define MAXSIZE 10

class CString {
private:
    char *dp;
    int len = 0;
    int max_capacity = MAXSIZE;
    void ensureCapacity();

public:
    CString() {
        dp = new char[max_capacity];
    }
    CString(char *s) {
        append(s);
    }
    CString &append(char c);
    CString &append(char *s);
    CString &append(CString cs);
    CString &append(char *s, int len);
    char *subString(int s_index, int e_index); //[s_index,e_index)
    CString removeSubStr(int s_index, int e_index);
    void show();
};

#endif




//cstring.cpp
#include "cstring.h"

void CString::ensureCapacity() {
    char *oldStr = dp;
    max_capacity *= 2;
    dp = new char[max_capacity];
    for (int i = 0; i < len; i++) {
        dp[i] = oldStr[i];
    }
    delete oldStr;
}
CString &CString::append(char c) {
    if (len == max_capacity) {
        ensureCapacity();
    }
    dp[len++] = c;
    return *this;
}
CString &CString::append(char *s) {
    while (*s) {
        append(*s);
        s++;
    }
    return *this;
}
CString &CString::append(CString cs) {
    char *cp = cs.dp;
    for (int i = 0; i < cs.len; i++) {
        append(*cp);
        cp++;
    }
    return *this;
}
CString &CString::append(char *s, int len) {
    while (*s && len--) {
        append(*s);
        s++;
    }
    return *this;
}
char *CString::subString(int s_index, int e_index) {
    if (s_index < 0) {
        printf("s_index is out of bounds!\n");
        return nullptr;
    }
    if (e_index >= len) {
        printf("e_index is out of bounds!\n");
        return nullptr;
    }
    int len = e_index - s_index;
    char *cp = new char[len];
    int i = 0;
    while (len--) {
        cp[i] = dp[s_index + i];
        i++;
    }
    cp[i] = 0;
    return cp;
}
CString CString::removeSubStr(int s_index, int e_index) {
    if (s_index < 0) {
        printf("s_index is out of bounds!\n");
        return *this;
    }
    if (e_index >= len) {
        printf("e_index is out of bounds!\n");
        return *this;
    }
    CString cs;
    cs.append(dp, s_index);
    cs.append(dp + e_index, len - e_index);
    return cs;
}

void CString::show() {
    printf("len=%d\n", len);
    for (int i = 0; i < len; i++) {
        printf("%c", dp[i]);
    }
    putchar(10);
}

//main.cpp
#include "cstring.h"
int main(int argc, char const *argv[]) {
    CString cs;
    cs.append("hello");
    cs.append(" ");
    cs.append("w");
    cs.append("o");
    cs.append("r");
    cs.append("l");
    cs.append("d");
    cs.show();
    cs.append("! haha");
    printf("%s\n", cs.subString(2, 5));

    cs.removeSubStr(2, 5).show();

    return 0;
}

举例:CString类-链表实现

//cstring.h
#ifndef _CSTRING_H_
#define _CSTRING_H_

#include <cstdio>
#include <cstring>
#include <iostream>

class Node {
public:
    char c;
    Node *prev;
    Node *next;
    Node(char c) {
        this->c = c;
        this->prev = nullptr;
        this->next = nullptr;
    }
};

class CString {
private:
    Node *head;
    Node *tail;
    int len;

public:
    CString() {
        head = nullptr;
        tail = nullptr;
        len = 0;
    }

    CString(char *s) {
        head = nullptr;
        tail = nullptr;
        len = 0;
        append(s);
    }
    CString &append(char c);
    CString &append(char *s);
    CString &append(CString cs);
    CString &append(char *s, int len);
    char *subString(int s_index, int e_index); //[s_index,e_index)
    CString removeNode(char c);
    CString removeSubStr(int s_index, int e_index);
    void show();
};

#endif

//cstring.cpp
#include "cstring.h"

CString &CString::append(char c) { // insert after
    Node *np = new Node(c);
    if (len == 0) {
        head = np;
        tail = np;
    } else {
        tail->next = np;
        np->prev = tail;
        tail = np;
    }
    len++;

    return *this;
}
CString &CString::append(char *s) {
    while (*s) {
        append(*s);
        s++;
    }
    return *this;
}
CString &CString::append(CString cs) {
    Node *tp = cs.head;
    while (tp) {
        append(tp->c);
        tp = tp->next;
    }

    return *this;
}
CString &CString::append(char *s, int len) {
    while (*s && len--) {
        append(*s);
        s++;
    }
    return *this;
}
char *CString::subString(int s_index, int e_index) {
    if (s_index < 0) {
        printf("s_index is out of bounds!\n");
        return nullptr;
    }
    if (e_index >= len) {
        printf("e_index is out of bounds!\n");
        return nullptr;
    }
    int len = e_index - s_index;
    Node *tp = head;
    while (s_index--) {
        tp = tp->next;
    }
    char *cp = new char[len + 1];
    int i = 0;
    while (len--) {
        cp[i++] = tp->c;
        tp = tp->next;
    }
    cp[i++] = 0;
    return cp;
}
CString CString::removeNode(char c) {
    // Node *np = new Node(c);
    Node *np = head;
    while (np) {
        if (np->c == c) {
            np->prev->next = np->next;
            np->next->prev = np->prev;
            np->prev = nullptr;
            np->next = nullptr;
            delete np;
            break;
        }
        np = np->next;
    }
    len--;
    return *this;
}
CString CString::removeSubStr(int s_index, int e_index) {
    if (s_index < 0) {
        printf("s_index is out of bounds!\n");
        return *this;
    }
    if (e_index >= len) {
        printf("e_index is out of bounds!\n");
        return *this;
    }

    char *cp = new char[len + 1];
    cp = subString(s_index, e_index);

    for (int j = 0; j < strlen(cp); j++) {
        removeNode(cp[j]);
    }

    return *this;
}

void CString::show() {
    Node *tp = head;
    int len = this->len;
    printf("---------------\n");
    printf("len:%d\n", len);
    while (len--) {
        printf("%c", tp->c);
        tp = tp->next;
    }
    putchar(10);
}

//main.cpp
#include "cstring.h"
int main(int argc, char const *argv[]) {
    CString cs;
    cs.append("hello");
    cs.append("w");
    cs.append("o");
    cs.append("r");
    cs.append("l");
    cs.append("d");
    cs.show();
    cs.append("! haha").show();

    printf("%s\n", cs.subString(2, 5));

    // cs.removeNode('w');
    // cs.removeNode('a');
    // cs.show();
    cs.removeSubStr(2, 5).show();

    return 0;
}

2 动态内存分配

/*
在C++中,动态内存分配技术可以保证程序在运行过程中按照实际需要申请适量的内存,使用结束后还可以释放,这种在程序运行过程中申请和释放的存储单元也称为堆对象。
在C++中,建立和删除堆对象使用的两个运算符:new和delete
new 数据类型(初始化参数列表);
该语句在程序运行过程中申请用于存放指定类型数据的内存空间,并根据初始化参数列表中给出的值进行初始化。如果内存分配成功,new 运算符返回一个指向新分配内存的首地址;如果申请失败,会抛出异常。
如果建立的对象是一个基本数据类型,初始化过程就是赋值,例如:
int * ap = new int(2);
动态分配了用于存放int型数据的内存空间,并将初值2存入该空间中,然后将首地址赋给指针ap。
对于基本数据类型,如果不希望在分配内存后设定初值,可以把括号省去。例如:
int * ap = new int;
如果保留括号,但括号中不写任何数值,则表示用0对该对象初始化,例如:
int * ap = new int(); 

如果建立的对象是某类的实例对象,就要根据初始化参数列表调用相应的构造函数。
如果用new 建立一个类的对象时,如果该类存在默认的构造函数,则 new T和new T()这两种写法效果是相同的,都会调用这个默认构造函数。

但若用户定义默认的构造函数,使用new T创建对象时,会调用系统生成的隐含的默认构造函数;
使用new T()创建对象时,系统除了执行默认构造函数外,还会为基本数据类型和指针类型的成员用0赋值,而且这一过程是递归的。
也就是说,如果该对象的某个成员也没有用户定义的默认构造函数,那么对该成员对象的基本数据类型和指针类型的成员,同样会以0赋值

int* ap = new int()[1,2,3]; //错误
int *ap=new int[5]{1,2,3,4,5};//创建数组并初始化
delete 指针名 ;
运算符delete用来删除由 new 建立的对象,释放指针所指向的内存空间。如果删除的是对象,该对象的析构函数将被调用。
对于用new 建立的对象,只能使用delete进行一次删除操作,如果对同一内存空间多次使用delete进行删除,将会导致运行时错误。

用new分配的内存,必须用delete加以释放,否则会导致动态分配的内存无法回收,使得程序占据的内存越来越大,这叫做“内存泄漏”



为节省存储空间,C++创建对象时仅分配用于保存数据成员的空间,而类中定义的成员函数则被分配到存储空间中的一个公用区域,由该类的所有对象共享。
*/
/*
对于基本数据类型	int *ap=new int;表示不给其设初值
				 int *ap=new int();表示初值为0
				 
对于类的对象,存在默认构造函数时   new T;和new T();都会调用默认构造函数,后者会初始化成员为0(包括指针)
*/

3 深浅拷贝

/*
在使用一个对象对另一个对象初始化或赋值时,复制构造函数能对对象的属性进行复制。
但如果存在指针成员,则指针的值会被复制,导致两个对象的指针成员指向同一块内存,在释放的时候对象的时候,会重复释放,导致程序崩溃。
若对象包含指针成员变量,则需要手动的编写拷贝构造函数实现深拷贝,调用编译器的内部默认的拷贝构造函数则只能实现浅拷贝操作。

在默认情况下(用户没有定义,但是也没有显示的删除),编译器会自动隐式生成一个拷贝构造函数和赋值运算符,但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象,但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。
这种区别从两者的名字也能轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;
赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。
调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生,如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
*/

浅拷贝导致错误

//错误:同一块内存重复释放

//student.h
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <stdio.h>
#include <string.h>

class Student {
public:
    Student() {}
    Student(char *name, int age) {
        this->name = new char[30];
        memset(this->name, 0, 30);
        strcpy(this->name, name);
        this->age = age;
    }
    /*
    Student(Student &s) {
        this->name = new char[30];
        memset(this->name, 0, 30);
        strcpy(this->name, s.name);
        this->age = s.age;
    }*/
    ~Student() {
        delete this->name;
    }
    void show() {
        printf("Student[name=%s,age=%d]\n", name, age);
    }

private:
    char *name;
    int age;
};

#endif



//main.cpp
#include "student.h"
#include <iostream>
#include <stdio.h>

int main(int argc, char const *argv[]) {
    Student s1("waiccc", 20);

    Student s2 = s1;
    s1.show();
    s2.show();

    return 0;
}



/*
Student[name=waiccc,age=20]
Student[name=waiccc,age=20]
free(): double free detected in tcache 2
已放弃 (核心已转储)
*/
/*
解决方法:重写复制构造函数
	Student(Student &s) {
        this->name = new char[30];
        memset(this->name, 0, 30);
        strcpy(this->name, s.name);
        this->age = s.age;
    }
*/

4 类的组合

/*
类的组合描述的就是一个类内嵌其它类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。

当创建类的对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将需要被创建。
在创建对象时,既要对本类的基本类型成员进行初始化,又要对内嵌对象成员进行初始化。

组合类构造函数的一般定义:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表), ...
其中,“内嵌对象1(形参表),内嵌对象2(形参表), ...”称为初始化列表,其作用是对内嵌对象进行初始化。
对基本类型的数据成员也可以这样初始化。

在创建一个组合类的对象时,不仅它自身的构造函数将被执行,而且还将调用其内嵌的对象的构造函数。这时构造函数调用顺序如下:
调用内嵌对象的构造函数,调用顺序按照内嵌对象的组合类的定义次序。
注意:内嵌对象在构造函数的初初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
执行本类的构造函数的函数体。
*/

参数列表

/*
类名(形参1,形参2):实参1(形参1),实参2(形参2)
*/
class Line {
public:
    Line() {}
    Line(Point a, Point b) : start(a), end(b) {
        // this->start = a;
        // this->end = b;
    }
    double length() {
        return start.distance(end);
    }

private:
    Point start;
    Point end;
};

前置声明

/*
不能在内联函数中使用该类。只能在类之外定义函数
类里声明,内外定义
*/
class B; //没有这句会报错

class A {
public:
    void f(B b);
};

class B {
public:
    void f(A a);
};

5 结构体与位域

/*
结构体是一种特殊形态的类,它和类一样,可以有自己的数据成员和函数成员,可以有自己的构造函数和析构函数,可以控制访问权限,可以继承,支持包含多态等。
二者定义形式也几乎一样。
唯一区别在于,结构体和类具有不同的默认访问控制属性:
    在类中,对于未指定访问控制权限的成员,其访问控制属性为私有类型(private);
    在结构体中,对于未指定任何访问控制属性的成员,其访问控制属性为公有类型(public)。
*/





/*
位域:

长度最小的char和bool在内存中都占据一个字节的空间。

C++允许在类中声明位域。
位域是一种允许将类中的多个数据成员打包,从而使不同的成员可以共享相同的字节的机制。在类中定义位域的方式为:
	数据类型说明符 成员名:位数;
	
位数指定一个位域所占用的二进制位数。使用位域,有以下几点需要注意:
1 C++并未规定打包的具体方式,因此不同的编译器会有不同的方式,不同编译器下,包含位域的类所占用的空间也不同。
2 只有bool, char,int, enum的成员才能够被定义为位域。
3 位域虽然节省了内存空间,但由于打包和解包过程中需要耗费额外的操作,所以运行时间很可能会增加。
*/
#include <iostream>
using namespace std;
enum Level { FRESHMAN,
             SOPHOMORE,
             JUNIOR,
             SENIOR };
enum Grade { A,
             B,
             C,
             D };

class Student {
public:
    Student(unsigned number, Level level, Grade grade) : number(number), level(level), grade(grade) {}

private:
    unsigned number : 27;// 类型一定要为unsigned,否则如果最高位为1,会被解释为负数
    Level level : 2; 
    Grade grade : 2;
};

int main() {
    Student s(123456578, SOPHOMORE, B);
    cout << sizeof(s) << endl;
}

6 静态方法 类的定义使用

const 1

/*
C/C++中const关键字的作用
	const修饰普通变量 C只读变量  C++常量
	C++:const修饰成员函数,该成员函数只能访问成员变量,不能修改,需要修改成员变量时需要加mutable修饰成员
	C++:const修饰一个对象,称之为常对象,只能调用const修饰的成员函数。
	
对于const修饰的类成员,只能在定义const成员时赋值或者在构造方法的初始化列表中赋值,不能在构造方法中赋值,不能在其他任何地方赋值。
对于const修饰的类成员,不能在构造方法的初始化列表中初始化。
*/

static 1

/*
static 修饰类成员时,需要在类外初始化,该类实例化的所有对象均可访问;访问形式:对象名.类成员名、类名::类成员名

static 修饰类函数时(静态成员函数、类的成员函数),属于类;不能访问非静态成员变量(静态成员函数属于类,所以没有默认的this指针生成)
*/

/*
C/C++中static的作用
	修饰局部变量或者局部对象,延长生命周期;
	修饰全局变量或者全局对象,只能在本文件中访问;
	修饰普通函数,该函数只能在本文件访问,不能在其他文件访问;
	
	修饰类的成员变量时,该变量为静态变量,属于类,被该类的所有实例化对象共享,不占用对象的空间;
	修饰成员函数时,该函数为静态成员函数,属于类,不属于对象,被该类的所有实例化对象共享,没有this指针,不能访问类的非静态成员;
*/
/*
何时使用static修饰类的成员或者成员函数?
	C/C++混合编程时,C的接口需要一个函数地址作为参数时,只能将类的非静态成员函数变为静态成员函数。
*/

举例:进制转换

// 10进制->其他任何进制

//stack.h
#ifndef _STACK_H_
#define _STACK_H_

#include <cstdio>
#include <iostream>

using namespace std;

#define MAX_SIZE 100

class Stack {
public:
    Stack() : top(-1) {
    }
    void push(char data) {
        if (top == MAX_SIZE - 1) {
            printf("stack is full!");
            exit(1);
        }
        this->top++;
        this->data[top] = data;
    }
    char pop() {
        if (top == -1) {
            printf("stack is empty!");
            exit(1);
        }
        char c = data[top];
        top--;
        return c;
    }
    int size() {
        return top + 1;
    }
    void show() {
        printf("----------------------\n");
        printf("len=%d\n", top + 1);
        for (int i = top; i >= 0; i--) {
            cout << data[i];
        }
        cout << endl;
    }
    char *getData() {
        char *s = (char *)malloc(sizeof(char) * (top + 1));
        int j = 0;
        for (int i = top; i >= 0; i--) {
            s[j++] = data[i];
        }
        s[j++] = 0;
        return s;
    }

private:
    int top;
    char data[MAX_SIZE];
};

#endif


#include "stack.h"

using namespace std;

char *change(int num, int jz) {			//   1  Stack change(int num,int jz)
    Stack s;
    while (num) {
        int remain = num % jz;
        if (jz == 16) {
            if (remain > 9) {
                remain -= 10;
                remain += 'A';
            } else {
                remain += '0';
            }
        } else {
            remain += '0';
        }
        s.push((remain));
        num /= jz;
    }
    return s.getData();  				//    2   return s;
    									//   以上两条语句可以返回结构体类型
}

int main(int argc, char const *argv[]) {
    Stack s;

    puts(change(10000, 16));
    puts(change(1000, 16));
    puts(change(100, 16));
    puts(change(10, 16));
    return 0;
}

/*
2710
3E8
64
A
*/

六 数据的共享与保护

/*
C++是适合编写大型复杂程序的语言,数据的共享与保护机制是C++的重要特性之一。
面向对象的程序设计方法兼顾数据的共享与保护,将数据与操作数据的函数封装在一起,构成集成度更高的组件或模块。
同一个对象的数据成员可以被该对象的任何一个函数访问。
对象与对象之间也需要共享数据,静态成员解决了同一个类的不同对象之间数据共享和函数共享的问题。
*/

1 作用域

/*
作用域是一个标识符在程序中有效的范围。C++中标识符的作用域有函数原型作用域,局部作用域,类作用域和命名空间作用域。

1 函数原型作用域
    是C++程序中的最小作用域,在函数原型声明时形式参数的作用范围就是函数的原型作用域。如:
    double area(double radius);
    标识符radius的作用范围就在函数area形参列表的左右括号之间。在程序的其它地方不能引用这个标识符。
    由于在函数原型的形参列表中起作用的只是形参类型,标识符并不起作用,因此是允许省去的。
    
2 局部作用域
	
*/
img
/*
	a, b, c都具有局部作用域,只是它们分别属于不同的局部作用域。由花括号确定有效范围。
	具有局部作用域的变量也称为局部变量。

3 类作用域
    类可以看作是一组成员的集合,类X的成员m具有类作用域,对m的访问方式有如下3种:
    1) 如果在X的成员函数中没有声明的同名的局部作用域标识符,那么在该函数内可以直接访问成员m
    2) 通过表达式x.m或X::m来访问,X::m适用于访问静态成员
    3) 通过ptr->m这样的表达式,其中ptr是指向X类一个对象的指针
    
4 命名空间作用域
    一个大型的程序通常由不同模块构成,不同模块甚至有可能由不同开发人员完成。不同模块中的类有可能发生重名,这样会引发错误。
    
    命令空间就像给类加了一个前缀,以示区别,好比,在缺乏上下文的环境下,说“南京路”,会产生歧义。
    但如果说“上海的南京路”或“武汉的南京路”,歧义就会消失。命名空间就起着这样的作用。
    
	一个命名空间确定了一个命名空间作用域,凡是在该命名空间之内声明的,不属于前面所述的各个作用域的标识符,都属于该命名空间作用域。
	在一个命名空间中,要引用其它命名空间的标识符,可以采用下面的方式:
		SomeNS :: SomeClass obj1; //声明一个SomeNS空间下的SomeClass类的对象obj1
		
	有时,在标识符前使用这样的命名空间限定会显得过于冗长,为了解决这一问题,C++提供了using语句,有以下两种形式:
    1) using 命名空间::标识符;
    2) using namespace 命名空间;
    前一种形式,将指定的标识符暴露在当前的作用域内,使得在当前作用域中可以直接引用该标识符
    后一种形式,将指定命名空间的所有标识符暴露在当前的作用域内,使得在当前作用域中可以直接引用该命名空间内的任何标识符
    
    事实上,C++标准程序库的所有标识符都被声明在std命名空间下,
    前面用的cin, cout, endl都是如此,因此前面的程序都使用了 using namespace std; 
    如果去掉这条语句,则需要使用std::cin, std::cout这样的语法
*/

2 命名空间

/*
全局空间和匿名命名空间

全局命名空间是默认的命名空间,在显式声明的命名空间之外声明的标识符都在一个全局命名空间中。
匿名命名空间是一个需要显示声明的没有名字的命名空间,声明方式如下:
namespace {
    各种声明
}
在包含多个源文件的工程中,匿名命名空间常常被用来隐藏不希望暴露给其它源文件的标识符。
这是因为每个源文件的匿名命名空间是彼此不同的,在一个源文件中没有办法访问其他源文件的匿名命名空间。
*/

匿名命名空间

/*
.h文件与.cpp文件。.h文件不是编译单元,所以不能直接编译.h文件;
.cpp/.c文件才是编译单元,编译器可以直接编译.cpp/.c文件。

那么如果一个.cpp文件include了.h文件,则在编译前会把所有include的.h的文件内容直接复制到该.cpp文件中,再对该.cpp文件编译。

这就意味着,如果在一个头文件中定义了unnamed namespace,
那么所有include该.h的编译单元(.cpp/.cc文件),都会完全包含该.h文件中的所有内容,
那么,一个.cpp中包含的匿名空间里的数据,它自己当然能访问了。
*/
namespace {
    class Point {
    public:
       Point(float x, float y):x(x),y(y){}
       Point(Point& p){
           printf("copy constructor:%x\n", p);
           x = p.x;
           y = p.y;
       };
       void show(){
           printf("show:%x, x=%f, y=%f\n", this, this->x, this->y);
       }
       friend class B;
    private:
       float x, y;
    };
}

int main() {
    Point p(1, 1);
    p.show();
    return 0;
}

//输出
show:361ad648, x=1.000000, y=1.000000

创建被其他文件引用的命名空间


//在a.h 中声明命名空间
#ifndef A_H_
#define A_H_

namespace jason {
    extern int a;

    void test();
}

#endif  A_H_ 

//在a.cpp中实现命名空间
int jason::a = 99;

void jason::test(){
    printf("hello:%d\n", jason::a);
}

//在b.cpp中引用
#include "a.h"

int main(){
    printf("jason::a=%d\n", jason::a);
    jason::test();
}

3 生存期

/*
静态生存期

如果对象的生存期与程序的运行期相同,则称它具有静态生存期。在命名空间作用域中声明的对象都是具有静态生存期的。如果要在函数内部局部作用域中声明具有静态生存期的对象,则要使用关键字static。局部作用域中的静态变量的特点是,它并不会随着每次函数调用而产生一个副本,也不会随着函数返回而失效。也就是说,当一个函数返回后,下一次再调用时,该变量还会保持上一回的值,即使发生递归调用,也不会为该变量建立新的副本,该变量在每次调用间共享。
注意:
static成员函数不包含this指针
static成员函数不能为virtual
不能存在static和non-static成员函数有相同的名字和参数
static 成员函数不能被声明成const、volatile或者const volatile



动态生存期

除上面两种情况,其余都是动态生存期。局部生存期对象诞于声明点,结束于声明所在的块执行完毕。
类的成员对象也有各自的生存期,不用static 修饰的成员对象,其生存期都与它们所属的对象生存期保持一致
*/

4 友元函数

/*
一个类是不能访问另一个类的私成成员的。
友元关系提供了不同类或对象的成员函数之间,类的成员函数与一般函数之间进行数据共享的机制。通俗的说:
	友元关系就是一个类主动声明哪些类或函数是它的朋友,进而给它们提供对本类私有成员的访问特许
在一个类中,可以利用关键字friend将其它函数或类声明为友元。

如果友元是一般函数或类的成员函数,称为友元函数;
如果友元是一个类,则称为友元类,友元类的所有成员函数都自动成为友元函数。
*/
#include <cmath>
using namespace std;
class Point {
public:
   Point(float x, float y):x(x),y(y){}
   Point(Point& p){
       printf("copy constructor:%x\n", p);
       x = p.x;
       y = p.y;
   };
   void show(){
       printf("show:%x, x=%f, y=%f\n", this, this->x, this->y);
   }
   friend float dist(Point &p1, Point &p2);
private:
   float x, y;
};

float dist(Point &p1, Point &p2){
    float x = p1.x - p2.x;
    float y = p1.y - p2.y;
    return sqrt(x*x + y*y);
}

int main() {
    Point p1(1, 1), p2(2, 2);
    cout << "The distance is: " << dist(p1, p2) << endl;
    return 0;
}
//友元关系不能传递,友元是单向的,友元关系不能被继承


//不能将类的私有函数作为其它类的友元函数
#include <cmath>
#include <iostream>
#include <stdio.h>
using namespace std;

class B {
private:
    int a;

public:
    void display();
};

class A {
private:
    int a;
    void display() {
        printf("this.a=%d\n", this->a);
    }

public:
    A(int a) {
        this->a = a;
    }
    friend void B::display();
};

void B::display() {
    A a(1);
    a.display();
    printf("B::display...\n");
}

int main() {

    A a(2);
    B b;
    b.display();
    return 0;
}

/*
this.a=1
B::display...
*/

5 const

/*
const修饰的变量或成员不可修改;
const修饰的函数在函数体内不可以修改任何变量值;
const修饰引用,则指向的对象不能被修改。
const修饰成员函数就是承诺不会修改该函数所属对象。

void test(const int a); //a 不可修改
void test() const ;     //test内部不可修改test()函数所在类的成员,不可调用非const成员函数。
						//此种情况只限于成员函数,非成员函数,不能有const, volatile修饰符
						
const int* test();      //返回值为指针时,指针所指向的变量不可修改
const int test();       //返回值不是指针,const无效


*/
/*
对于除指针以外的其他常量声明句法来说, 
const type name 和 type const name
的效果是相同的, 即都声明一个类型为type名为name的常量,如: 
const int x = 1;和int const x = 1;
还有
int x = 1;const int &y = x;和int const &y = x;
都是等效的, 只是写法的风格不同而已, 有人喜欢用const type name, 比如STL的代码; 
另外一些人喜欢写成type const name, 比如boost中的大量代码, 其实效果都是一样的。

*/

/*
对于指针来说, const出现在*号的左边还是右边或是左右都有才有区别, 具体的:

const type *p; // 一个不能修改其指向对象的type型指针     不能修改p.什么
// 其实和type const *p等效

type * const p; // 一个不能修改其自身指向位置的type型指针    不能p=。。

const type * const p; 
// 一个既不能修改其指向对象也不能修改其自身指向位置的type型指针
// 也和type const * const p等效

而C++中的引用当引用了一个值后,就不能把其修改为引用另外一个值,
这相当于type * const p的指针, 如:

int a, b;
int &c = a; // 声明时就和a绑定在一起
c = b; // 是把b的值赋给a, 而不是把b作为c的另一个引用

所以引用的功能其实和type * const p指针是相同的, 即其引用(指向)的位置不可改变,
如果声明引用时再加上const, 其实就相当于const type * const p型的指针了,
所以声明引用时,const type &name 和 type const &name等效的
*/

6 访问修饰符

/*
public:在类的可见范围内,该成员可以被访问
protected:只允许子类及本类的成员函数访问
private:只允许本类的成员函数访问
protected与private只在继承中有本质区别
*/
#include <cmath>
#include <iostream>
#include <stdio.h>
using namespace std;

class A {
public:
    A() { printf("构造。。\n"); }
    ~A() { printf("析构。。\n"); }

private:
    double balance;

protected:
    void testA() {
        printf("---testA()---\n");
    }
};
class B : public A {
public:
    void test() {
        testA();
    }
};
int main() {
    A a;
    B b;
    b.test();
    return 0;
}

七 运算符重载

/*

!!!!!!!!!!!输入输出运算符只能类外重载,采用友元形式!!!!!!!!!!


运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
运算符重载的本质是函数重载。
你可以重定义或重载大部分 C++ 内置的运算符。例如 + 、 - 、 * 、 / 、
++、--、>>、<<等,这样,你就能使用自定义类型的运算符。

格式:
返回类型 operator 运算符名称(形参列表)
{
    重载实体;
}
可以把上面的 [ operator 运算符名称 ] 看作新的函数名。


规则:
1 C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载;
2 C++中不允许重载的运算符
    1) 成员运算符 .
    2) 成员选择运算符 .*
    3) 域解析运算符 ::
    4) 条件运算符 ?:
    5) 类型大小运算符 sizeof
3 重载不能改变运算符运算对象(即操作数)的个数;如,关系运算符“>”和“<”等是双目运算符,重载后仍为双目运算符,需要两个参数。
4 运算符”+“,”-“,”*“,”&“等既可以作为单目运算符,也可以作为双目运算符,可以分别将它们重载为单目运算符或双目运算符。
5 重载不能改变运算符的优先级别;
6 重载不能改变运算符的结合性。如,复制运算符”=“是右结合性(自右至左),重载后仍为右结合性;
7 重载运算符的函数不能有默认的参数;
8 重载运算符的运算中至少有一个操作数是自定义类;
9 对运算符的重载,不应该失去其原有的意义;

*/

举例:分数

//类内重载
//fraction.h
#ifndef __FRACTION_H_
#define __FRACTION_H_
#include <iostream>
#include <stdio.h>
class Fraction {
private:
    int fenzi;
    int fenmu;
    int maxYue(int a, int b);
    int minBei(int a, int b);
    Fraction yue(Fraction c);

public:
    Fraction(int fz, int fm);
    Fraction();
    void show();
    /*
    Fraction add(Fraction a, Fraction b);
    Fraction min(Fraction a, Fraction b);
    Fraction mul(Fraction a, Fraction b);
    Fraction chu(Fraction a, Fraction b);
    */
    Fraction operator+(Fraction &f) {
        Fraction c;
        c.fenmu = this->fenmu * f.fenmu;
        c.fenzi = this->fenzi * f.fenmu + f.fenzi * this->fenmu;
        return yue(c);
    }
    Fraction operator-(Fraction &f) {
        Fraction c;
        c.fenmu = this->fenmu * f.fenmu;
        c.fenzi = this->fenzi * f.fenmu - f.fenzi * this->fenmu;
        return yue(c);
    }
    Fraction operator*(Fraction &f) {
        Fraction c;
        c.fenmu = this->fenmu * f.fenmu;
        c.fenzi = this->fenzi * f.fenzi;
        return yue(c);
    }
    Fraction operator/(Fraction &f) {
        Fraction c;
        Fraction d;
        d.fenzi = f.fenmu;
        d.fenmu = f.fenzi;
        c = *this * d;
        return yue(c);
    }
};

#endif

//fraction.cpp
#include "fraction.h"

int Fraction::maxYue(int a, int b) {
    int i;
    for (i = a > b ? b : a; i > 1; i--) {
        if (a % i == 0 && b % i == 0) {
            return i;
        }
    }
    return 1;
}
int Fraction::minBei(int a, int b) {
    int i;
    for (i = a > b ? a : b; i <= a * b; i++) {
        if (i % a == 0 && i % b == 0) {
            break;
        }
    }
    return i;
}
Fraction Fraction::yue(Fraction c) {
    int x = maxYue(c.fenzi, c.fenmu);
    c.fenmu /= x;
    c.fenzi /= x;
    return c;
}
Fraction::Fraction(){};
Fraction::Fraction(int fz, int fm) {
    this->fenzi = fz;
    this->fenmu = fm;
}
void Fraction::show() {
    if (fenzi == 0) {
        printf("%d\n", 0);
    } else if (fenzi == fenmu) {
        printf("%d\n", 1);
    } else {
        printf("%d/%d\n", fenzi, fenmu);
    }
}
/*
Fraction Fraction::add(Fraction a, Fraction b) {
    Fraction c;
    c.fenmu = a.fenmu * b.fenmu;
    c.fenzi = a.fenzi * b.fenmu + b.fenzi * a.fenmu;
    return yue(c);
}
Fraction Fraction::min(Fraction a, Fraction b) {
    Fraction c;
    c.fenmu = a.fenmu * b.fenmu;
    c.fenzi = a.fenzi * b.fenmu - b.fenzi * a.fenmu;
    return yue(c);
}
Fraction Fraction::mul(Fraction a, Fraction b) {
    Fraction c;
    c.fenmu = a.fenmu * b.fenmu;
    c.fenzi = a.fenzi * b.fenzi;
    return yue(c);
}
Fraction Fraction::chu(Fraction a, Fraction b) {
    Fraction c;
    Fraction d;
    d.fenzi = b.fenmu;
    d.fenmu = b.fenzi;
    c = c.mul(a, d);
    return yue(c);
}
*/

//main.cpp
#include "fraction.h"
#include <iostream>
#include <stdio.h>

int main(int argc, char const *argv[]) {

    printf("input fz/fm:\n");
    int x, y;
    scanf("%d/%d", &x, &y);
    Fraction fs1(x, y);

    printf("input fz/fm:\n");
    scanf("%d/%d", &x, &y);
    Fraction fs2(x, y);

    printf("+:");
    (fs1 + fs2).show();

    printf("-:");
    (fs1 - fs2).show();

    printf("*:");
    (fs1 * fs2).show();

    printf("/:");
    (fs1 / fs2).show();

    return 0;
}

举例:

//类外重载
class Complex {
public:
    Complex(){}
    Complex(int r, int v){
        this->r = r;
        this->v = v;
    }

    Complex operator+(Complex& c){   //类内重载
        int r = this->r + c.r;
        int v = this->v + c.v;
        Complex cs(r, v);
        return cs;
    }

    friend Complex operator-(const Complex&, const Complex&);

    friend ostream& operator<<(ostream&, const Complex&);

    friend istream& operator>>(istream&, Complex&);

private:
    int r, v;
};

Complex operator-(const Complex &a, const Complex &b){  //作为友元函数实现类外重载
    int r = a.r - b.r;
    int v = a.v - b.v;
    Complex cs(r, v);
    return cs;
}

ostream& operator<<(ostream& os, const Complex& c){
    int r = c.r;
    int v = c.v;
    if(r == 0){
        os << v << "i";
    }else if(v == 0){
        os << r;
    }else if(v < 0){
        os << r << v << "i";
    }else{
        os << r << "+" << v << "i";
    }
    return os;
}

istream& operator>>(istream& is, Complex& c){
    int r, v;
    scanf("%d%di", &r, &v);
    getchar();
    c.r = r;
    c.v = v;
    return is;
}

int main(){
    Complex c1, c2;
    printf("Input one complex:\n");
    cin >> c1;
    cout << c1 <<endl;

    printf("Input another complex:\n");
    cin >> c2;
    cout << c2 <<endl;

    cout << "sum=" << (c1+c2) << endl;
    cout << "diff=" << (c1-c2) << endl;

    return 0;
}

/*
cout<<c 会被解释成 operator<<(cout, c)
参数 os 只能是 ostream 的引用,而不能是 ostream 对象,因为 ostream 的复制构造函数是私有的,没有办法生成 ostream 参数对象。operator<< 函数的返回值类型设为 ostream &,并且返回 os,就能够实现<<的连续使用,如cout<<c<<5

ERROR:  must take exactly one argument
类内重载二元运算符时,只需要一个参数,另一个参数由this指针传入,这里如果需要传入两个参数,需要放到类外定义,声明友元(访问私有成员)。

类内声明 >>的例子:
istream& operator>>(istream& is){
    int r, v;
    scanf("%d%di", &r, &v);
    getchar();
    this->r = r;
    this->v = v;
    return is;
}

c1 >> cin;
*/

举例:重载输入输出

/**/
#include <iostream>
#include <stdio.h>

using namespace std;

class A {

public:
    A(int a, int b) {
        this->a = a;
        this->b = b;
    }
    friend ostream &operator<<(ostream &os, A &);
    friend istream &operator>>(istream &is, A &);

private:
    int a;
    int b;
};
ostream &operator<<(ostream &os, A &a) {
    os << a.a << "/" << a.b;
    return os;
}
istream &operator>>(istream &is, A &t) {
    int a, b;
    scanf("%d/%d", &a, &b);
    getchar();
    t.a = a;
    t.b = b;
    return is;
}
int main(int argc, char const *argv[]) {
    A a(3, 2);
    cout << a << endl;
    cin >> a;
    cout << a << endl;
    return 0;
}

举例:前置后置++

class Complex {
public:
    Complex(int r, int v){
        this->r = r;
        this->v = v;
    }
    Complex operator+(Complex& c){   //类内重载
        int r = this->r + c.r;
        int v = this->v + c.v;
        Complex cs(r, v);
        return cs;
    }
    Complex& operator++(){//前加
        this->r++;
        this->v++;
        return *this;
    }
    Complex operator++(int) {//后加
        Complex temp(this->r, this->v);
        this->r++;
        this->v++;
        return temp;
    }
    void show(){
        if(r == 0){
            printf("%di\n", v);
        }else if(v == 0){
            printf("%d\n", r);
        }else if(v < 0){
            printf("%d%di\n", r, v);
        }else{
            printf("%d+%di\n", r, v);
        }
    }

private:
    int r, v;
};

int main() {
    Complex c1 (1, 1);
    Complex c2 (2, 2);
    //(c1++ + c2).show();
    (++c1 + c2).show();
}

八 继承与派生

1 基本概念

/*
编程,很大程度上是为了描述和解决现实世界中的问题。C++中的类很好的采用了人类思维中的抽象和分类方法,类和对象的关系恰当反映了个体与同类群体共同特征的关系。进一步观察现实世界可以看到,不同事物之间也是联系着的,继承便是其中一种联系。C++使用继承实现的代码的进一步复用,以及多态等强大功能,增强了程序的灵活性和扩展能力。

面向对象程序设计提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体,更强大的类的定义。
以原有类为基础产生新的类,原有类叫做父类或基类,扩展出来的新类叫做子类或派生类。父类和子类都是相对的。

类的继承和派生的层次结构,可以说是人们对自然界中事物进行分类,分析和认识的过程在编程中的体现。
比如对交通工具的分析,最高层是抽象程度最高的,是最具有普通和一般意义的概念,下层具有了上层的特性,同时也加入了自己的特性,而最下层是最为具体的。在这个层次结构中,由上而下,是一个个体化,特殊化的过程。由下而上,是一个逐步抽象,泛化的过程。

*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kt0H6Xwy-1678436419471)(D:\1aaaasuqian\day\img\000187.gif)]

/*
以银行账户为例,它具有账号,余额等信息。
有两类账户,一种是借记卡账户,可存可取,可以计算利息。
另一种是信用卡账户,可存可取,不计算利息,但可以透支,需要支付给银行利息。
这时,我们就可以创建一个Account的基类,再派生出两个子类,即信记卡账户和信用卡账户,它们之间有很多共同之处,也有各自的特点。
*/



//继承语法
/*
假设基类Base1和Base2是已经定义的类,下面的语句定义了一个名为Derived的派生类,该类由Base1和Base2派生而来。

*/
class Derived : public Base1, private Base2 {
public:
    Derived ();
    ~Derived ();
}
/*
一个派生类,可以同时有多个基类,这种情况称为多继承,这时派生类同时得到了多个类已有的特征

在派生过程中,派生出来的新类可以作为基类再继续派生出新的类,此外,一个基类可以同时派生出多个派生类。
这样就形一个相互关联的类的家族,有时也称为类族。
在类族中,直接参与派生出新类的基类称为直接基类,基类的基类,或更高层的基类称为间接基类。

在派生类的定义中,除了要指定基类外,还需要指定继承方式。
继承方式规定了如何访问从基类继承的成员。
在派生类的定义语句中,每个继承方式只限定紧随其后的基类。
继承方式有public, protected和private,如果不显式的给出继承方式,系统默认为私有继承。

派生类成员是指:包括从基类继承来的成员和新增加的数据和成员函数。这些新增加的成员,正是派生类不同于基类的关键所在,是对基类的发展
*/


/*
派生过程::
在C++ 程序设计中,派生新类的过程,实际经历了3个步骤:吸收基类的成员,改造基类成员,添加新的成员。
面向对象的继承和派生机制,最重要的目的是实现代码重用和扩充。
因此,吸收基类成员是一个重用的过程,而对基类成员进行调整,改造以及添加新成员就是原有代码扩充的过程。二者相辅相乘。

*/

举例:两个基类

#include <iostream>
#include <stdio.h>

using namespace std;

class A {
public:
    int a1;
    A(int a1) : a1(a1) {}
};

class B {
public:
    int b1;
    B(int b1) : b1(b1) {}
};

class C : public A, public B {
public:
    C(int num1, int num2) : A(num1), B(num2) {}
    void show() {
        printf("a=%d b=%d\n", a1, b1);
    }
};

int main(int argc, char const *argv[]) {
    C c(1, 9);
    c.show();
    return 0;
}
//a=1 b=9

2 Virtual

/*
在c++中,基类必须将它的两种成员函数区分开来
(1)一种是基类希望其派生类进行覆盖(override)的函数。这种函数,基类通常将其定义为虚函数(加virtual)。
	当我们使用基类的指针或者引用调用虚函数时,该调用将被动态绑定。
(2)另外一种是基类希望派生类直接继承而不需要改变的函数。

基类通过在其成员函数的声明语句之前加关键字virtual使得该函数执行动态绑定。

任何构造函数之外的非静态函数都可以是虚函数。





基类的析构函数为什么要用virtual虚析构函数?   has virtual method 'test' but non-virtual destructor

C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。
假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。
那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
*/


举例:

#include <iostream>
#include <stdio.h>

using namespace std;

class A {
public:
    int a1;
    A(int a1) : a1(a1) {}
    virtual void show() {
        printf("A:a1=%d \n", a1);
    }
    virtual ~A() {
        printf("~A...\n");
    }
};

class C : public A {
public:
    C(int num1) : A(num1) {}

    ~C() {
        printf("~C...\n");
    }
    void show() {
        printf("C:a1=%d \n", a1);
    }
};

int main(int argc, char const *argv[]) {
    // C c(1);
    A *aa = new C(1);
    aa->show();
    delete aa;

    return 0;
}
/*
A为父类,C为子类
A *aa = new C(1);
aa->show();		//父类中的show使用virtual修饰,子类也写了show函数,此时调用子类中的show
				//父类中的show没有virtual修饰,子类也写了show函数,此时调用父类中的show
				//父类中的show没有virtual修饰,子类无show函数,此时调用父类中的show
*/

3 如何只在堆\栈上创建对象

只在堆中创建

/*
当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。
当对象使用完后(离开作用域,所占内存自动释放),然后会自动调用析构函数。

编译器管理了对象的整个生命周期。
如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。

编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
因此,将析构函数设为私有,类对象就无法建立在栈上了。

这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。
类中必须提供一个destory函数,来进行内存空间的释放。类对象使用完成后,必须调用destory函数。


当构造函数或者析构函数被私有化,则只能在堆中创建对象;
使用指针指向的方法创建对象,并且需要一个共有的destroy方法,用来在创建完成之后销毁该对象。
*/
举例:
#include <cstdio>
#include <iostream>
using namespace std;

class Person {
public:
    Person() {}
    void test() {
        printf("test..addr=%p\n", this);
    }
    void destroy() {
        delete this;
    }

private:
    ~Person() {
        printf("destructor...\n");
    }
    int age;
    string name;
};

int main(int argc, char const *argv[]) {
    Person *p = new Person;
    p->test();
    p->destroy();
    return 0;
}

/*
也可以将构造函数,析构函数都设为private或protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。
在堆中的对象,使用完后,必须调用delete释放内存,此时,会自动去调用析构函数。
*/
举例:
#include <cstdio>
#include <iostream>
using namespace std;

class Person {
public:
    static Person *create() {
        return new Person;
    }
    void test() {
        printf("test..addr=%p\n", this);
    }
    void destroy() {
        delete this;
    }

private:
    Person() {}
    ~Person() {
        printf("destructor...\n");
    }
    int age;
    string name;
};

int main(int argc, char const *argv[]) {
    Person *p = Person::create();
    p->test();
    p->destroy();
    return 0;
}

只在栈中创建

/*
如果想让对象只能在栈上,那就是不能让别人使用到new这个操作符。可以在class中重载了私有的成员函数new,而且delete也需要一起重载下。如下面的例子:
*/

举例
#include <cstdio>
#include <iostream>
using namespace std;

class Person {
public:
    void test() {
        printf("test..addr=%p\n", this);
    }
    void destroy() {
        delete this;
    }
    Person() {}
    ~Person() {
        printf("destructor...\n");
    }

private:
    int age;
    string name;

    void *operator new(size_t t) {
    }
    void operator delete(void *ptr) {
    }
};

int main(int argc, char const *argv[]) {
    Person p;
    p.test();
    return 0;
}

4 类型兼容原则

/*
在需要基类对象的任何地方,都可以使用公有派生类的对象来代替。
通过公有派生,派生类得到了除构造函数,析构函数,私有成员之外的所有成员。这种规则体现在:

1 派生类的对象可以隐含转换为基类对象
2 派生类的对象可以初始化基类的引用
3 派生类的指针可以隐含转换为为基类的指针
*/
class A {
};

class B: public A {
};

int main(){
    A a, * pa;
    B b;
    a = b;
    A& ra = b;
    pa = &b;
}
/*
虽然根据类型兼容原则,可以将派生类对象的地址赋值给基类的指针,但是通过这个基类类型的指针,却只能访问到从基类继承的成员,而不是派生类自己的同样的成员函数。 
由于类型兼容的特性存在,可以在基类对象出现的场合使用派生类对象进行替换,但是替换之后派生类仅仅能发挥出基类的作用。
在后续章节,将学习另一个重要特性--多态。多态的设计方法可以保证在类型兼容的前提下,基类和派生类的对象,分别以不同的方式响应相同的消息。
*/

5 派生类的构造和析构函数

/*
继承的目的是为了发展,派生类继承了基类的成员,实现了原有代码的重用,这只是一个目的,而代码的扩充才是最主要的,只有通过添加新的成员,加入新的功能,类的派生才有实际意义。

由于基类的构造函数和析构函数不能被继承,在派生类中,如果对派生类新增的成员进行初始化,就必须为派生类添加新的构造函数。
但是派生类的构造函数只负责对派生类的新增成员进行初始化,对所有从基类继承来的成员,其初始化工作还是由基类的构造函数完成。
同样,对派生类对象的清理工作也需要加入新的析构函数。


*/
/*
派生类对于基类的很多成员(私有)是不能直接访问的,因此要完成对基类成员的初始化工作,需要调用基类的构造方法。
派生类的构造函数需要以合适的初值作为参数,其中一些参数要传递给基类的构造函数,用于初始化相应的成员,另一些参数要用于派生类新增成员的初始化。

在构造派生类对象时,会首先调用基类的构造函数,来初始化它们的数据成员,然后按照构造函数初始化列表中指定的方式初始化派生类新增的成员,最后再执行派生数构造函数的函数体。

什么时候需要声明派生类的构造函数?如果对基类初始化时,需要调用基类的有参构造函数时,派生类就必须声明构造方法
提供一个将参数传递给基类构造函数的途径,保证在基类对象初如化时能获得必要的数据。
如果不需要调用基类带参构造方法,也不需要调用新增成员对象的带参构造方法,派生类也可以不声明构造函数。全部采用默认构造函数。
*/



/*
//派生类执行构造函数的一般次序
1 如果参数是类类型,调用复制构造函数,把实参值复制到形参中
2 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左到右)
3 对派生类的新增成员对象初始化,调用顺序按照他们在类中的声明顺序(根据情况,调用构造函数或复制构造函数)
4 执行派生类的构造函数体


构造函数初始化列表中基类名,对象名之间的顺序无关紧要,无论他们的顺序是怎样的,基类构造函数和各个成员对象的初始化顺序都是确定的
*/



/*
在派生过程中,基类的析构函数也不能继承下来,如果需要在析构的时候做一些事的话,就要在派生类中声明新的析构函数。
它的任务是清理好派生类中新增的非对象成员(比如指针)。析构函数的调用次序与构造函数的调用次序正好相反。

*/


/*
如果在派生类中声明了与继承来的同名的标识符,则继承来标识符不可见。
如果是函数,重载不影响,重写的话会隐藏。如果要访问被隐藏的成员,就需要使用基类名和作用域限定符来访问。
如果派生类有多个父类,并且继承了多个父类的相同的方法(方法体不同),在派生类访问需要加父类名和作用域限定符。
*/

6 虚基类

/*
如果某个派生类的部分或全部直接基类是从另一个共同基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域限定符来唯一标识,而且必须用直接基类来限定。
*/
img
#include <cstdio>
#include <iostream>
using namespace std;

class Base0 {
public:
    int var0;
    void fun0() {
        printf("Base0...\n");
    }
};

class Base1 : public Base0 {
};
class Base2 : public Base0 {
};

class Derived : public Base1, public Base2 {
};

int main(int argc, char const *argv[]) {

    Derived d;

    d.Base1::fun0();
    d.Base1::var0 = 1;
    d.Base2::var0 = 2;

    printf("Base1::var0=%d\n", d.Base1::var0);
    printf("Base2::var0=%d\n", d.Base2::var0);

    return 0;
}

/*
这种情况下,派生类对象在内存中就同时拥有成员a的两份同名拷贝。
对于数据成员来说,虽然两个a可以存放不同的值,也可以使用作用域限定符来区分并访问,但很多情况下,我们只需要一个这样的数据副本。
同一成员的多份副本增加了内存开销。C++中提供了虚基类技术来解决这一问题。

上例中其实Base0的成员函数fun0()的代码只有一个副本,之所以调用fun0()函数,仍需要需要直接基类名加以限定,是因为调用非静态函数,必须用对象来调用。

上例中,Derived 类的对象中存在两个Base类的子对象,即Base1的对象和Base2的对象。
因此调用show()函数时,需要使用Base1或Base2加以限定,这样才能明确针对哪个Base对象的调用。
*/
/*
当某类的部分或全部基类是从另一个共同基类派生而来时,这些直接基类从上一级共同基类中继承来的成员就拥有相同的名称。

在派生类对象中,这些同名数据成员就拥有多个副本,可以使用作用域限定符来唯一标识并访问他们,也可以将共同基类设置为虚基类,这时从不同路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射,这样就解决了同名成员的唯一标识问题。
class 派生类名:virtual 继承方式 基类名
*/
#include <cstdio>
#include <iostream>
using namespace std;

class Base0 {
public:
    int var0;
    void fun0() {
        printf("Base0...\n");
    }
};

class Base1 : virtual public Base0 {
};
class Base2 : virtual public Base0 {
};

class Derived : public Base1, public Base2 {
};

int main(int argc, char const *argv[]) {

    Derived d;

    d.var0 = 1;
    printf("var0:%d\n", d.var0);

    /*
        d.Base1::fun0();
        d.Base1::var0 = 1;
        d.Base2::var0 = 2;

        printf("Base1::var0=%d\n", d.Base1::var0);
        printf("Base2::var0=%d\n", d.Base2::var0);
    */
    return 0;
}

构造对象一般顺序

/*
1 如果构造方法中有类类型参数,则先执行拷贝构造函数
2 如果该类有直接或间接的虚基类,则先执行虚基类的构造函数
3 如果该类有其他基类,则按照它们在继承声明列表中出现的次序,分别执行它们的构造函数,但构造过程中,不再执行它们的虚基类构造函数(有参构造方法的调用,需要在初始化列表中声明)
4 按照在类定义中出现的次序,对派生类中新增的成员对象进行初始化。对于类类型的成员对象,如果出现在构造函数的初始化列表中,则以其中指定的参数执行构造函数或复制构造函数,如未出现,则执行默认构造函数;对于基本数据类型的成员,如果出现在构造函数的初始化列表中,则使用其中指定的值为其赋初始,否则什么也不做。
5 执行构造函数的函数体
*/

7 派生类对象的内存布局

/*
在类中,不仅成员变量占一定空间,若有虚函数,还有有额外8字节的指针,指向虚函数表

派生类对象的内存布局需要满足的要求是,一个基类指针,无论其指向基类对象还是派生类对象,通过它来访问一个基类中定义的数据成员,都可以使用相同的步骤。
*/


//单继承情况
/*
class Base{...}
class Derive : public Base {...}
那么在Derive类的对象中,从Base继承来的数据成员,全部放在前面,与这些数据成员在Base类对象中放置的顺序保持一致,Derive类的新增数据成员,全部放在后面。如下图所示。如果出现了从Derive指针到Base指针的转换,例如:
Base* pba = new Base();
Derive* pd = new Derive();
Base* pbb = pd;
*/
img
/*
在pd赋值给pbb的过程中,指针值不需要改变。pba和pbb这两个Base类型的指针,虽然指向的对象具有不同的类型,但任何一个Base成员到该对象首地址都具有同的偏移量,因此使用Base指针的pba和pbb访问Base类中定义的数据成员时,可以采用相同的方式,而无需考虑具体的对象类型。
*/


//多继承情况
/*
多继承的情况比单继承稍微复杂一些,考虑下面的情况:
class Base1 {...};
class Base2 {...};
class Derive : public Base1, public Base2 {...};
Derived类继承了Base1和Base2类,在Derive类的对象中,前面依次存放的是从Base1类和Base2类继承而来的数据成员,其顺序与它们在Base1类和Base2类的对象中放置的顺序一致,Derive类新增的数据放在它们的后面,如下图所示。如果出现了从Derive指针到Base1或Base2指针的隐含转换,例如:
Base1 * pb1a = new Base1();
Base2 * pb2a = new Base2();
Derive* pd = new Derive();
Base1* pb1b = pd;
Base2* pb2b = pd;
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MnAMSb5W-1678436419473)(D:\1aaaasuqian\day\img\000194.gif)]

/*
将pd赋值给pb1b指针时,与单继承时的情况相似,只需要把地址复制一遍即可。
但将pd赋值给pb2b指针时,则不能简单地执行地址复制操作,而应当在原地址的基础上加一个偏移量,使pb2b指针指向Derive对象中Base2成员的首地址。
这样,对于同为Base2类型指针的pb2a和pb2b来说,它们都指向Base2中定义的,以相同方式分布的数据成员。



通过上而的介绍,我们应该认识到一个与直观不符的结论:
    指针转换并非都保持原先的地址不变,地址的算述运算可能在指针转换时发生。但这又不是简单的地址算术运算,因为如果Derive指针的值为0,则转换后Base2指针值也应为0。因此,在将Derive指针转换为 Base2类型指针,执行的操作是,先判断原指针是否为0,如果是0,则以0作为转换后的指针值,否则以原地址加上一个偏移量后得到转换后的指针值。
*/

8 字节对齐

/*
编译器用'N'来设置数据的对齐方式。默认32位OS对齐字节是4,64位对齐字节是8。'N'有可能影响结构体内部成员的对齐位置,以及结构体整体大小。
对齐规则:
1 如果类包含虚函数,则对象最前面会有占8字节的地址,指向一个虚表,该表中包含该类每个虚函数的地址。
2 每个成员变量在其结构体内的偏移量都是“MIN(对齐字节,成员变量类型的大小)”的倍数。
3 如果有嵌套结构体,那么内嵌结构体的第一个成员变量在外结构体中的偏移量,是“MIN(对齐字节,内嵌结构体中那个数据类型大小最大的成员变量)”的倍数。
4 整个结构体的大小要是“MIN(对齐字节,这个结构体内数据类型大小最大的成员变量)”的倍数。如果有内嵌结构体,那么取“MIN(对齐字节,内嵌结构体中数据类型大小最大的成员变量)”作为计算外结构体整体大小的依据。
*/

9 类型转换

/*
dynamic_cast
用于向下转型,即将基类指针或引用转型为派生类指针或引用。如果指针实际上指向派生类对象,则转型成功,返回子类对象地址,否则失败,返回NULL。由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表,因此要有虚函数,
当我们将dynamic_cast用于某种类型的指针或引用时,只有该类型至少含有虚函数时(最简单是基类析构函数为虚函数),才能进行这种转换。否则,编译器会报错。
//动态转换必须存在虚函数,

*/
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

class Human {
public:
    int age;
    char *name;
    virtual void show() {
        printf("name=%s,age=%d\n", name, age);
    }
    Human(int age, char *name) : age(age), name(name) {
    }
};

class Man : public Human {
public:
    Man(int age, char *name) : Human(age, name) {
    }
};

int main(int argc, char const *argv[]) {

    Human *hp = new Man(20, "zhang");
    hp->show();
    Man *mp = (Man *)dynamic_cast<Man *>(hp);
    
    /*
    Human *hp=new Human(20,"zhang");
    Man *mp=(Man *)dynamic_cast<Human*>(hp);
    也可以实现
    */

    mp->show();

    return 0;
}


/*
static_cast
用于向下转型,基实就是强制转换。不管基类指针是否指向子类对象,一律把指针转型后返回。转换不保证安全()。
1) 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
2) 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
3) 把空指针转换成目标类型的空指针。
4) 把任何类型的表达式转换成void类型。

*/
//Human *hp = new Man(20, "zhang");
//Man *mp = static_cast<Man *>(hp);
Human *hp = new Human(20, "zhang");
hp->show();
Man *mp = static_cast<Man *>(hp);
/*


const_cast   去除变量的const限定
在C++里,把常量指针(即指向常量的指针)赋值给非常量指针时,会提示错误,这时候就需要用到const_cast
const int i = 100;
int * ip = &i; //报错
int * ip = const_cast<int*>(&i); //正确


reinterpret_cast
可以用在"没有关系"的类型之间,而用static_cast来处理的转换就需要两者具有"一定的关系"了。
就是不理会数据本来的语义,而重新赋予它新的语义。
*/

举例:

#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

class Human {
public:
    int age;
    char *name;
    virtual void show() {
        printf("name=%s,age=%d\n", name, age);
    }
    Human(int age, char *name) : age(age), name(name) {
    }
};

class Man : public Human {
public:
    int m;
    Man(int age, char *name, int m) : Human(age, name), m(m) {
    }
};

int main(int argc, char const *argv[]) {

    printf("sizeof(Human):%ld\n", sizeof(Human));
    Man *hp = new Man(20, "zhang", 111);
    printf("hp=%p\n", hp);
    printf("hp->age.addr=%p\n", &hp->age);
    printf("hp->name.addr=%p\n", &hp->name);
    printf("hp->m.addr=%p\n", &hp->m);

    long *p = reinterpret_cast<long *>(hp);

    p++;
    printf("age=%ld\n", *p);
    p++;
    printf("name=%s\n", *p);
    p++;
    printf("m=%ld\n", *p);

    return 0;
}

10 虚函数表

/*
C++中的虚函数的作用是实现运行时多态。在基类中声明一个虚(virtual)函数,然后在派生类中对其进行重写。
基类的引用或者指针指向一个派生类对象,当该基类变量调用该函数时候,会自动调用派生类的函数,这就是所谓的动态多态。

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。
简称为vtbl。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,其内容真实反映了实际应该调用函数。
这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类对象的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

C++是通过虚函数表来实现运行时多态的。
通常所有声明为virtual的虚函数地址都被存放于该表中。
编译器会为每个存在虚函数的类对象插入一个vtpr(virtul function pointer),该vptr指向存放了虚函数地址的虚函数表vtbl,这样对象在调用虚函数的时候,第一步会先根据vptr找到vtbl,然后根据该虚函数在vtbl中的索引来进行调用,这样就实现了运行时多态功能。

*/
img
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

class Human {
public:
    int age;
    char *name;
    virtual void show() {
        printf("[name=%s,age=%d,addr=%p]\n", name, age, this);
    }
    Human(int age, char *name) : age(age), name(name) {
    }
};

class Man : public Human {
public:
    int m;
    Man(int age, char *name, int m) : Human(age, name), m(m) {
    }
};

typedef void (*Fun)();

int main(int argc, char const *argv[]) {

    Man man(22, "zhagn", 123);
    long address2 = *(long *)&man; 					// vtbl address
    // printf("vtbl address=%lx\n", address);		//虚函数表地址不可打印
    Fun fun2 = (Fun)(*(long *)address2);
    fun2();

    Man *mp = new Man(20, "wang", 111);
    long address = *(long *)mp; 					// vtbl address
    // printf("vtbl address=%lx\n", address);		//虚函数表地址不可打印
    Fun fun = (Fun)(*(long *)address);
    fun();

    return 0;
}
/*
[name=zhagn,age=22,addr=0x7ffd88f287c0]
[name=wang,age=20,addr=0x557fdcc302c0]
*/

九 多态

1 概述

/*
多态是指同样的消息被不同类型的对象接收时,导致不同的行为。所谓消息是指对对象成员函数的调用,不同的行为指不同的函数实现。
比如+号运算符,可以用于多种类型数据的相加操作,同样是+号运算符,对于不同的对象,发生的行为也不同。
如果不同类型的变量相加,例如浮点型和整型,则要先将整型转为浮点型,然后再进行加法运算,这就是典型的多态现象。
C++中的多态包括四种:
1 重载多态:函数重载和运算符重载
2 强制多态,强制类型转换
3 包含多态,用虚函数实现,在子类中重写函数,以实现动态绑定
4 参数多态,与类模板相关联,后面章节再介绍




多态从实现角度讲可以分为:编译时多态和运行时多态

前者是在编译过程中确定的同名操作的具体操作对象;而后者则是在程序运行过程中动态地确定操作所针对的对象。这种确定操作的具体对象的过程叫做绑定。按照绑定进行的阶段不同,可以分为两种绑定方法:静态绑定和动态绑定,这两种绑定过程分别对应着多态的两种实现方式。
1 绑定在编译连接阶段完成的情况称为静态绑定。由于绑定是在程序运行之前进行的,所以也叫早期绑定或前绑定。有些多态类型,绑定(确定操作对象的过程)在编译,链接阶段完成,就是静态绑定,比如重载,强制和参数多态。
2 在编译,连接过程中无法解决绑定问题,需要等到程序运行之后才能确定,就是动态绑定,包含多态就是通过动态绑定完成的。
*/
/*
C++中多态实现的三个条件
1 必须存在一个继承体系结构
2 继承体系结构中的一些类必须具有同名的virtual成员函数(virtual是关键字)
3 至少有一个基类类型的指针或基类类型的引用。这个指针和引用可以对virtual成员函数进行调用
*/

2 抽象类

/*
有时,在基类中某些函数,无法给出有意义的实现。对于这种在基类中无法实现的函数,能否在基类中只说明函数原型,用来统一接口,而在派生类中再给出具体实现呢?在C++中提供了纯虚函数来实现这一功能。
格式为:
virtual 返回类型 函数名(参数表)= 0;

它与一般虚函数的原型在书写格式上,就在于后面多个“= 0”。声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。纯虚函数的函数体由派生类给出
纯虚函数不同于函数体为空的虚函数;纯虚函数根本就没有函数体。前者所在的类是抽象类,不能直接实例化,而后者所在的类是可以实例化的。它们的共同特点是都可以派生出新类,然后在新类中给出虚函数的实现,可以实现多态。
带有纯虚函数的类是抽象类,抽象类的主要作用是建立一组公共的接口,而本身并不去实现,由派生类实现。
抽象类派生出新类后,如果派生类给出所有纯虚函数的实现,这个派生类就可以实例化,否则仍然是抽象类。抽象类不能实例化。
*/

十 模板

1 函数模板

/*
C++最重要的特性之一就是代码重用,为了实现代码重用,代码必须具有通用性。通用代码应不受数据类型的限制,并且可以自适应数据类型的变化。
这种程序设计方法称为参数化程序设计。

模板是C++支持参数化设计的工具,通过它可以实现参数化多态性。
参数化多态,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象



函数模板本身在编译时不会生成任何目标代码,只有由模板生成的实例会生成目标代码
被多个源文件引用的函数模板,应当连同函数体一同放在头文件中,而不能像普通函数那样只将声明放在头文件中。
函数指针也能指向模板的实例,而不能指向模板本身。
*/
#include <iostream>
#include <stdio.h>

using namespace std;

template <typename T>

T abs(T x) {
    return x < 0 ? -x : x;
}

int main(int argc, char const *argv[]) {
    int a = 1;
    double b = -2.5;
    cout << abs(a) << endl;
    cout << abs(b) << endl;

    return 0;
}
/*
这两个函数只有参数类型不同,功能完全相同(函数体一样)。
如果能写一段通用代码,能适合于多种不同数据类型,便会使代码的可重用性大大提高,从而提高软件的开发效率。
使用函数模板可以实现这一目的。
程序员只需要对函数模板编写一次,然后基于调用函数时提供的参数类型,C++编译器将自动产生相应的函数来正确处理该类型的数据。
*/
#include <iostream>
#include <stdio.h>

using namespace std;

template <typename T>

T abs(T x) {
    return x < 0 ? -x : x;
}

class Point {
public:
    int x;
    int y;
    Point(int x, int y) : x(x), y(y) {}
    friend ostream &operator<<(ostream &os, Point t);
};
ostream &operator<<(ostream &os, Point t) {
    os << "[" << t.x << "," << t.y << "]";
    return os;
}
template <typename T>
void show(T *p, int len) {
    for (int i = 0; i < len; i++) {
        cout << *(p + i) << " ";
    }
    cout << endl;
}
int main(int argc, char const *argv[]) {
    Point pa[] = {Point(1, 1), Point(2, 2), Point(3, 3)};
    show(pa, sizeof(pa) / sizeof(pa[0]));
    /*
    int a = 1;
    double b = -2.5;
    cout << abs(a) << endl;
    cout << abs(b) << endl;

    int arr[] = {1, 2, 3, 4, 5};
    show(arr, sizeof(arr) / sizeof(int));

    char c[] = "abcdef";
    show(c, sizeof(c) / sizeof(char));
    */
    return 0;
}

/*
函数模板本身在编译时不会生成任何目标代码,只有由模板生成的实例会生成目标代码
被多个源文件引用的函数模板,应当连同函数体一同放在头文件中,而不能像普通函数那样只将声明放在头文件中。
函数指针也能指向模板的实例,而不能指向模板本身。
*/

2 类模板

/*
类模板是后期C++加入的一种可以大大提高编程效率的方法。
使用类模板使用户可以为类定义一种模式,使得类中的某些数据成员,某些成员函数的参数,返回值或局部变量能取任意类型(包括基本数据类型和用户自定义的)。由于类模板需要一种或多种类型参数,所以类模板也常称为参数化类。


一个类模板声明并不是一个类,它说明了类的一个家族。
只有当被其它代码引用时,模板才根据需要生成具体的类
*/
#include <iostream>
#include <stdio.h>

using namespace std;

template <typename T>
class List {
public:
    virtual void add(T dat) = 0;
    virtual T get(int index) = 0;
    virtual int size() = 0;
    virtual void travel() = 0;
    virtual ~List() {}
};

template <typename T>
class ArrayList : public List<T> {
private:
    int len;
    T *dp;
    int capacity;
    void ensureCapacity() {
        T *tp = dp;
        capacity *= 2;
        dp = new T[capacity];
        for (int i = 0; i < len; i++) {
            *(dp + i) = *(tp + i);
        }
        delete[] tp;
    }

public:
    ArrayList() {
        len = 0;
        capacity = 10;
        dp = new T[capacity];
    }
    void add(T dat) {
        if (len == capacity) {
            ensureCapacity();
        }
        *(dp + len++) = dat;
    }
    T get(int index) {
        if (index < 0 || index >= len) {
            printf("index out of bounds!\n");
            exit(1);
        }
        return dp[index];
    }
    int size() {
        return len;
    }
    void travel() {
        printf("--------------------------\n");
        printf("size=%d\n", len);
        for (int i = 0; i < len; i++) {
            cout << *(dp + i) << " ";
            if ((i + 1) % 10 == 0) {
                putchar(10);
            }
        }
        cout << endl;
    }
    ~ArrayList() {
        delete[] dp;
    }
};

class Point {
public:
    Point() {}
    Point(int a, int b) : a(a), b(b) {}
    friend ostream &operator<<(ostream &os, Point t);

private:
    int a;
    int b;
};
ostream &operator<<(ostream &os, Point t) {
    os << "[" << t.a << "," << t.b << "]";
    return os;
}
int main(int argc, char const *argv[]) {

    ArrayList<Point> list;
    for (int i = 1; i <= 100; i++) {
        list.add(Point(i, i));
    }
    list.travel();

    ArrayList<int> list1;
    for (int i = 1; i <= 100; i++) {
        list1.add(i);
    }
    list1.travel();

    /*
    ArrayList<double> list2;
    for (int i = 1; i <= 100; i++) {
        list2.add(i * 0.1);
    }
    list2.travel();

    ArrayList<char> list3;
    for (int i = 1; i <= 100; i++) {
        list3.add(i + 31);
    }
    list3.travel();
    */

    return 0;
}

十一 智能指针

1 auto_ptr

/*
C++的内存管理是让很多人头疼的事,一不小心就会发生内存泄漏,重复释放,野指针等问题。

大部分关于指针的问题都是来源于堆空间,为什么呢?
我们知道栈上的空间是由系统维护的,申请和释放的工作都是由系统根据栈的性质来完成的,不需要我们过多干预。而堆上空间的申请(new)和释放(delete)都必须由程序员显示的调用,并且很重要的一点,这段空间的生命周期就在new 和 delete 之间。
但是我们不能避免程序还未执行到delete就跳转了,或者在函数中没有执行到delete语句就返回了,如果我们不在每一个可能跳转和返回之前释放内存,就会造成内存泄漏。

所以程序员必须很仔细的申请和释放对象所占内存,但是由于程序的复杂度增大,判断、循环、递归这样的语句会让程序的走向不确定。
很有可能出现内存泄露的问题。
使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。
*/

/*
什么是RAII:资源获取即初始化
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。这样做有两个好处:
1 我们不需要再显示的释放资源
2 采用这种方式,对象所需的资源在其生命期内始终保持有效

在C++中它被应用的实例除了这里的智能指针,还有C++11中的lock_guard 对互斥锁的管理也是典型的RAII机制。
但是我们要注意RAII != 智能指针,它只是解决问题的一种思想。智能指针顾名思义,它肯定要有指针的行为,因此我们还需要把*和 ->进行重载。
*/


/*
简单总结一下就是auto_ptr可用来管理单个对象的内存,控制权可以随意转换但只有一个在用,不要轻易使用operator=() 或者拷贝构造,使用了就不要再访问之前对象的数据。所以说它的一些设计也不太符合C++的编程设计,所以这个指针已经被C++淘汰了。
*/

2 unique_ptr

/*
unique_ptr 是一种简单粗暴的智能指针,既然你拷贝和赋值会造成很多问题,那就不要拷贝了。所以它是独享所有权的,不让你拷贝和赋值。
*/

3 shared_ptr

/*
auto_ptr有缺陷,unique_ptr不能拷贝,如果真的要实现拷贝,C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr的原理:是通过引用计数的方式实现对象共享资源。
shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
*/
/*
shared_ptr类在 memory头文件中
智能指针也是模板,所以在创建一个智能指针时,必须提供指针指向的类型:
shared_ptr<string> p1;
shared_ptr<list<int> > p2;

1 默认初始化的智能指针中保存着一个空指针。
2 使用方式与普通指针类似,解引用一个智能指针返回它指向的对象。
3 如果在一个条件判断中使用智能指针,效果就是检测它是否为空




shared_ptr和unique_ptr都支持的操作:
1 shared_ptr<T> sp 、unique_ptr<int> up   空智能指针,可以指向类型为T的对象
2 p  将p用作一个条件判断,若p指向一个对象,则为true
3 *p 解引用p,获得它指向的对象
4 p->mem 等价于(*p).men
5 p.get() 返回p中保存的指针,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
6 swap(p,q) 交换p和q中指针 ,p.swap(q)



shared_ptr独有的操作:
1 make_shared<T> (args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象。
2 shared_ptr<T>p(q)  p是shared_ptr q的拷贝:此操作会递增q中的计数器。q中的指针必须能转换为T*
3 p=q  p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数。若p的引用计数变为0,则将其管理的原内存释放
4 p.unique() 若p.use_count()为1,返回true,否则返回false
5 p.use_count()  返回与p共享对象的智能指针数量:可能很慢,主要用于调试


make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数 
//指向一个值为666的int的shared_ptr
shared_ptr<int> p1 = make_shared<int>(666);
//指向一个值为“999999999”的string
shared_ptr<string> p2 = make_shared<string>(10,'9');
//指向一个值初始化的int,即值为0;
shared_ptr<int> p3 = make_shared<int>();
cout << *p1 << endl;//666
cout << *p2 << endl;//9999999999
cout << *p3 << endl;//0


shared_ptr和new结合使用
接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须用直接初始化的方式来初始化一个智能指针:
shared_ptr<int> p1 = new int(1024); 	//错误
shared_ptr<int> p2(new int(1024)); 		//正确
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存。


定义和改变shared_ptr的其他方法
1 shared_ptr p(q)    p管理内置指针q所指向的对象;q必须是指向new分配的内存,且能够转换为T*类型
2 shared_ptr p(u)     p从unique_ptr u那里接管了对象的所有权:将u置为空
3 shared_ptr p(q,d)    p接管了内置指针q所指向的对象所有权,q必须能够转换为T*类型。p将使用可调用对象d来代替delete
4 p.reset() ,p.reset(q)   若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空

release方法返回一个unique_ptr管理的内置指针,并将unique_ptr置空,一般用release的返回值来直接初始化另一个智能指针,因为release只是切断了智能指针与对象之间的联系,但是并没有释放该对象所占空间,因此我们要保证这块空间被另一个智能指针管理,否则我们就要手动管理内存了。
release和reset的核心区别就一点,release放弃对原内存的管理,但不释放原内存,reset放弃原内存的管理转而去管理另一块内存,且同时释放原内存。
*/


/*
不要混合使用普通指针和智能指针
shared_ptr可以协助对象的析构,但这仅限于自身的拷贝之间。
推荐使用make_shared而不是new的原因:
在分配对象的同时就将shared_ptr与之绑定,从而避免无意中将同一块内存绑定到多个独立创建的shared_ptr上多个独立的shared_ptr指向同一块内存,会发生内存泄漏问题:

int * a = new int(999);
shared_ptr<int> p1(a);
shared_ptr<int> p2(a);
cout << *p1 << endl;//999
cout << p1.use_count() << endl;//1
cout << *p2 << endl; //999
cout << p2.use_count() << endl;//1
//会发生内存泄漏,因为两个独立的shared_ptr同时指向了一个内存空间,该内存会被释放两次


正确做法:使用另一个智能指针初始化其他智能指针
int * a = new int(999);
shared_ptr<int> p1(a);
shared_ptr<int> p2(p1);//使用p1初始化p2,两个指针的计数器都为2,安全
cout << *p1 << endl;//999
cout << p1.use_count() << endl;//2
cout << *p2 << endl;//999
cout << p2.use_count() << endl;//2
值传递时,实参会被拷贝,会递增其引用计数,局部变量被释放后,计数减一
void test(shared_ptr<int> p) {
    ++*p;
    cout << "值传递时,引用计数加一:" << p.use_count() << endl;//2
}
int main(void) {
    int * a = new int(999);
    shared_ptr<int> p1(a);
 
    cout << *p1 << endl;//999
    cout << p1.use_count() << endl;//1
    
    test(p1);
    cout << *p1 << endl;//1000
    cout << p1.use_count() << endl;//1
    
    return 0;
}
*/

举例:shared_ptr

#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

template <class T>
class SharedPtr {
public:
    SharedPtr(T *ptr = NULL)
        : _ptr(ptr), _pCount(new int(1)), _pMutex(new mutex) {
        if (_ptr == nullptr)
            _pCount = 0;
    }
    ~SharedPtr() {
        Release();
    }
    // p(p1)
    SharedPtr(const SharedPtr<T> &s)
        : _ptr(s._ptr), _pCount(s._pCount), _pMutex(s._pMutex) {
        AddCount();
    }
    // p = p1;
    SharedPtr<T> &operator=(const SharedPtr<T> &s) {
        if (this != &s) {
            if (_ptr)
                // 释放管理的旧资源
                Release();
            _ptr = s._ptr;
            _pCount = s._pCount;
            // 如果是一个空指针对象,则不加引用计数,否则才加引用计数
            if (_ptr)
                AddCount();
        }
        return *this;
    }
    T &operator*() {
        return *_ptr;
    }
    T *operator->() {
        return _ptr;
    }
    int AddCount() {
        _pMutex->lock();
        ++(*_pCount);
        _pMutex->unlock();
        return *_pCount;
    }
    int SubCount() {
        _pMutex->lock();
        --(*_pCount);
        _pMutex->unlock();
        return *_pCount;
    }
    int UseCount() {
        return *_pCount;
    }
    T *Get() {
        return _ptr;
    }

private:
    void Release() {
        if (_ptr && --(*_pCount) == 0) {
            delete _ptr;
            delete _pCount;
        }
    }

private:
    T *_ptr;
    int *_pCount; // 引用计数
    mutex *_pMutex;
};

void TestSharedPtr() {
    SharedPtr<int> sp1(new int(10));
    SharedPtr<int> sp2(sp1);
    cout << sp1.UseCount() << endl; // 2
    cout << sp2.UseCount() << endl; // 2
    SharedPtr<int> sp3(new int(10));
    sp2 = sp3;
    cout << sp1.UseCount() << endl; // 1
    cout << sp2.UseCount() << endl; // 2
    cout << sp3.UseCount() << endl; // 2
    sp1 = sp3;
    cout << sp1.UseCount() << endl; // 3
    cout << sp2.UseCount() << endl; // 3
    cout << sp3.UseCount() << endl; // 3
}
int main() {
    TestSharedPtr();
    // getchar();
    return 0;
}

4 weak_ptr

/*
虽然shared_ptr是用来避免内存泄漏,可以自动释放内存。但是shared_ptr在使用中可能存在循环引用,使引用计数失效,从而导致内存泄漏的情况。

*/
#include <iostream>
#include <memory>

using namespace std;

class A {
public:
    std::shared_ptr<B> bptr;
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {//设定一个作用域
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout << "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
    return 0;
}
//循环引用导致ap和bp的引用计数都为2,在离开作用域之后,ap和bp的引用计数只减为1,而没有减为0,导致两个指针都不会被析构,产生内存泄漏。
/*
weak_ptr叫弱引用指针。
是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。
weak_ptr 设计的目的是为了协助shared_ptr而引入的一种智能指针,它可以解决shared_ptr循环引用的问题。
weak_ptr只可以从一个shared_ptr或另一个 weak_ptr 对象来构造, 它的构造和析构不会引起引用记数的增加或减少。
#include <iostream>
#include <memory>

using namespace std;

class A {
public:
    std::shared_ptr<B> bptr;
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {//设定一个作用域
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout << "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
    return 0;
}


循环引用导致ap和bp的引用计数都为2,在离开作用域之后,ap和bp的引用计数只减为1,而没有减为0,导致两个指针都不会被析构,产生内存泄漏。

*/
#include <iostream>
#include <memory>

using namespace std;

class A {
public:
    std::weak_ptr<B> bptr; // 修改为weak_ptr
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {//设定一个作用域
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout << "main leave" << endl; 
    return 0;
}

/*
上面代码中在对B的成员赋值时,即执行ap->bptr=bp时,由于bptr是weak_ptr,它并不会增加引用计数,所以bp的引用计数仍然会是1,在离开作用域之后,bp的引用计数为减为0,A指针会被析构,析构后其内部的aptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。
*/

/*
use_count():获取当前观察资源的引用计数:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; //输出1

expired():判断所观察资源是否已经释放:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
    cout << "weak_ptr无效,资源已释放";
else
    cout << "weak_ptr有效";

lock():lock()函数返回一个指向共享对象的shared_ptr,如果对象被释放,则返回一个空shared_ptr:
std::weak_ptr<int> gw;
void f()
{
    auto spt = gw.lock();
    if(gw.expired()) {
        cout << "gw无效,资源已释放";
    }
    else {
        cout << "gw有效, *spt = " << *spt << endl;
    }
}
int main()
{
    {
        auto sp = std::make_shared<int>(42);
        gw = sp;
        f();
    }
    f();
    return 0;
}
使用场景:weak_ptr不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。
这时就不能使用weak_ptr直接访问对象。那么可以用上面的代码首先判断weak_ptr指向对象是否存在?
*/

十二 异常处理

/*
在编写应用软件时,不仅要保证软件的正确性,而且应该具有容错能力。就是说,不仅能在正确的环境条件下,用户的正确操作时要运行正确,而且要在环境条件出现意外或用户操作不当的情况下,仍然能够正确合理的处理,不能轻易出现死机,崩溃,更不能出现灾难性的后果。
由于环境条件和用户操作的正确性是没有办法百分百保障的,所以在设计程序时,就要考虑到各种意外情况,并给予恰当的处理。这就是我们所说的异常处理。
程序在运行中有些错误是可以预料,但不可避免的,例如内存不足,网络没有连接好,打印机未连接好等由系统运行环境造成的错误。这时要力争做到:给出优雅合理的提示,允许用户排除环境错误或操作错误,程序依然可以继续正常运行,不崩溃。这就是异常处理的任务。

在一个大型软件中,函数各有分工,都是具体功能的载体,一个函数可以完成一个任务,整个项目的复杂功能就是由函数间相互调用来完成的。
如果函数在运行过程中出现了错误,它时,它就引发一个异常,希望他的调用者能够捕获这个异常并处理这个异常。如果调用者也不能处理这个错误,还可以继续传递给上级调用者去处理,这种传递会一直继续到异常被处理为止。如果程序始终没有处理这个异常,最终它会被传到C++运行系统那里,运行系统捕获异常后通常只是简单地终止这个程序。
下图说明了函数的调用关系和异常传播方向:
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A5xswpcO-1678436419474)(D:\1aaaasuqian\day\img\000192.gif)]

/*
C++的异常处理机制使得异常的引发和处理不必在同一函数中,这样底层的函数可以着重解决具体问题,而不必过多地考虑对异常的处理。上层调用者可以在适当的位置设计对不同类型异常的统一处理。
*/

/*
异常处理的语法

如果某段程序发现了自己不能或不想处理的异常,就可以使用throw表达式抛出这个异常,将它抛给调用者。throw的操作数表示异常类型,语法上与return语句的操作数类似。如果程序中有多种要抛出的异常,应该用不同的操作数类型来互相区别。
try子句后的复合语句是受保护的代码段。如果预料某段程序代码或对某函数的调用可能发生异常,就将它放在try块中。如果这段代码运行时真的发生异常,其中的throw表达式就会抛出这个异常。
catch子句是异常处理程序,捕获由throw表达式抛出的异常。异常声明部分指明了子句处理异常的类型和异常参数名称。类型可以是任何有效的数据类型,包括C++的类。当异常被抛出以后,catch子句便依次检查,若某个catch子句声明的异常类型与被抛出的异常类型一致,则执行该段代码。如果异常类型是一个省略号,catch子句便处理所有类型的异常,这段处理程序必须是catch子句的最后一个分支。



当以下条件之一成立时,抛出的异常与一个catch子句中声明的异常类型匹配

1 catch子句中声明的异常类型就是抛出的异常对象的类型或其引用
2 catch子句中声明的异常类型是抛出异常对象的类型的公共基类或其引用
3 抛出异常类型和catch子句中声明的异常类型皆为指针,且前者到后者可以隐含转换


总结:
1 异常在divide函数中被抛出,由于在divide函数中没有对异常进行处理,
2 catch处理程序的出现顺序很重要,因为在一个try块中,异常处理程序是按照它出现的次序被检查和匹配的。只要找到一个匹配的异常类型,后面的异常处理都将被忽略。所以catch(...)应该放在最后



有时,在函数内部处理异常,并不合适
int divide(int x, int y){
    if(y == 0){
        try{
            throw x;
        }catch(int a){
            printf("The error has been processed already ...\n");
        }
        return 0;
    }
    return x/y;
}

int main() {
    try{
        cout << "5/2=" << divide(5,2) << endl;
        cout << "8/0=" << divide(8,0) << endl;
        cout << "7/1=" << divide(7,1) << endl;
    }catch(int e){
        cout << e << " is divided by zero!" << endl;
    }catch(double e){
        cout << e << " is divided by zero! double" << endl;
    }catch(...){
        cout << "default...\n";
    }
    cout << "That is ok." << endl;

    return 0;
}
*/

举例:猜数字

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

void inputNum(int &number) {
    while (1) {
        cout << "please input a num:" << endl;
        cin >> number;
        /*						//确保输入的是数字,使用cin
        if (cin.fail()) {
            cin.clear();
            cin.ignore(10, '\n');
            continue;
        }
        */
        						//确保输入的是数字,使用scanf
        if (scanf("%d", &number) == 0) {
            // printf("%d\n", a);
            getchar();
            fflush(stdin);
            cout << "input char!" << endl;
            continue;
        }
        
        return;
    }
}
int main(int argc, char const *argv[]) {

    srand((unsigned int)time(NULL));
    int r = rand() % 10000 + 1;

    int answer;

    while (1) {
        inputNum(answer);
        if (answer > r) {
            cout << "大了!" << endl;
        } else if (answer < r) {
            cout << "小了!" << endl;
        } else {
            cout << "猜中了!" << endl;
            break;
        }
    }

    return 0;
}

/*
异常接口声明

为了增强程序的可读性和安全性,使函数的用户能够方便地知道所使用的函数会抛出哪些异常,可以在函数的声明中列出这个函数可能抛出的所有异常类型,例如:
void fun() throw (A, B, C, D);
如果在函数的声明中没有包括异常接口声明,则此函数可以抛出任何类型的异常,一个不抛出任何类型异常的函数可以这样声明:
void fun() throw ();
如果一个函数抛出了它的异常声明所不允许抛出的异常时, unexpected函数会被调用,该函数的默认行为是调用terminate函数中止程序。用户也可以定义自己的unexpected函数,替换默认函数。

int divide(int x, int y) throw( int){
    float b = 10;
    if(y == 0){
        throw b;
    }
    return x/y;
}

void myUnexpected(){
    printf("Bad Exception!\n");
    throw 0;
}

int main() {
    set_unexpected(myUnexpected);
}


异常处理中的构造与析构

C++异常处理的真正功能,不仅在于它能够处理各种不同类型的异常,还在于它具有为异常抛出前构造的所有局部对象自动调用析构函数的能力。这一过程称为栈的解旋。
class MyException{
public:
    MyException(const char* msg ){
        printf("Constructor of MyException...\n");
        strcpy(this->msg, msg);
    }
    MyException(const MyException& e){
        printf("Copy Constructor of MyException...\n");
        strcpy(this->msg, e.msg);//getMessage());
    }
    const char* getMessage() const {
        return msg;
    }
    ~MyException(){
        printf("Destructor of MyException...\n");
    }
private:
    char msg[100];
};

class Demo {
public:
    Demo(){
        printf("Constructor of Demo\n");
    }
    ~Demo(){
        printf("Destructor of Demo\n");
    }
};

void func() throw (MyException ){
    Demo d;
    printf("Throw MyException in func()\n");
    throw MyException("exception thrown by func()");
}

int main() {
    try{
        func();
    }catch(MyException e){
        printf("Caught an exception: %s, addr=%x\n", e.getMessage(), &e);
    }
    printf("Resume the execution of main()\n");

    return 0;
}


//抛出的是对象,catch中参数应该是相应的类类型
//抛出的是指针,如 throw new MyException; 这种,则catch中的参数应该是相应类的指针类型


用一个不带操作数的throw表达式,可以将当前正在被处理的异常再次抛出,这样一个表达式只能出现catch子句或catch子句内部调用的函数中,再次抛出的异常是源异常对象,不是副本

*/

/*

标准库异常处理

C++提供了一组标准异常类,这些类以基类Exception开始,标准程序库抛出的所有异常,都是派生于该基类,这些异常类的继承关系如下:

*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ucGtrjnw-1678436419474)(D:\1aaaasuqian\day\img\000196.gif)]

/*
该基类提供一个成员函数what(),用于返回错误信息,声明如下:
virtual const char* what() const throw();
下表列出了各个具体异常类的含义及定义他们的头文件。runtime_error和logic_error是一些具体异常类的基类,它们分别表示两大类异常。logic_error表示那些可以在程序中被预先检测到的异常,也就是如果精心地编写程序,这类异常能够避免;而runtime_error则表示那些难以避免的错误。
*/
img
/*
一些编译语言规定只能抛出某个类的派生类对象,如Java中的Exception类。C++中没有这个强制要求,但仍然可以这样实践。例如, 在程序中可以使所有的异常皆派生自Exception或其子类,或者直接抛出标准库提供的异常类型,这样做会带来很多方便
logic_error和runtime_error

这两个类及其派生类,都有一个接收const string&型参数的构造方法。在构造异常对象时需要将具体的错误信息传递给该函数,如果调用该对象的what函数,就可以得到构造时提供的错误信息。

*/

举例:三角形面积

#include <cstring>
#include <exception>
#include <iostream>
#include <math.h>
#include <stdio.h>
using namespace std;

double area(double a, double b, double c) {
    if (a <= 0 || b <= 0 || c <= 0) {
        throw invalid_argument("the side must bigger than zero!");
    }
    if (a + b <= c || a + c <= b || b + c <= a) {
        throw invalid_argument("tht sum of two side must bigger than another side!");
    }
    double p = (a + b + c) / 2;
    return sqrt(p * (p - a) * (p - b) * (p - c));
}

int main(int argc, char const *argv[]) {

    double a, b, c;
    printf("input three sides:\n");
    cin >> a >> b >> c;
    try {
        cout << "area=" << area(a, b, c) << endl;
    } catch (exception e) {
        printf("error:%s\n", e.what());
    }
    return 0;
}

十三 流类库与输入输出

/*
C++语言中,也没有输入输出语句。
但C++标准库中有一个面向对象的输入输出软件包,它就是I/O流类库。它是C语言中I/O函数在面向对象程序设计方法中的一个替代对象。

当程序与外界进行信息交换时,存在着两个对象,一个是程序中的对象,一个是文件对象。

流是一种抽象,它负责在数据的生产者和消费者之间建立联系,并管理数据的流动
程序建立一个流对象,并指定这个流对象与某个文件对象建立关联。然后,程序操作流对象,流对象通过文件系统对所关联的文件产生作用。因此:
流对象是程序和文件对象进行交互的界面,对程序而言,流对象就是文件对象的化身

操作系统将键盘,屏幕,打印机和通信端口都作为扩充文件来处理的,而这种处理是通过操作系统的设备驱动文件来实现的。因此,与这些设备的交互也是通过I/O流来实现的。

I/O流类库的基础是一组类模板。

流的基本单位除了普通字符外,还可以是其它类型,如wchar_t,流的基本单位的数据类型就是模板的参数。
C++的头文件中已经用typedef为这些I/O的类模板面向char类型的实例定义了别名。
由于模板的实例和类具有相同的性质,可以直接把这些别名看作流类的类名。

在I/O流类库中,头文件iostream声明了4个预定义的流对象用来完成标准设备上的输入输出操作:cin, cout, cerr, clog

下图给出了I/O流类库中面向char类型的各个类之间的关系,简要说明和相关头文件:
*/
img
/*

//输出流

最重要的3个输出流是ostream, ofstream和ostringstream。预先定久的ostream类对象用来完成向标准设备的输出。
1) cout是标准输出流
2) cerr是标准错输出流,没有缓冲,发送给他的内容立即被输出
3) clog类似于cerr,但是有缓冲,缓冲区满时被输出

char buf[10];
setbuf(stderr, buf);
clog << "hello";
_sleep(3);
clog << " world" << endl;


通过cout, cerr, clog输出的内容,在默认情况下都会输出到屏幕,标准输出和标准错误输出的区别在发生输出重定向时会表示出来。执行程序时可以在命令行使用 ">" 对标准输出进行重定向,这会使得通过cout输出的内容写到重定向的文件中,而通过cerr输出的内容仍然输出到屏幕;使用 "2>"可以对标准错误输出重定向,而不会影响标准输出。



很多操纵符(manipulator)都定义在ios_base类中(如hex()),以及iomanip头文件中(如(setpricision())
输出宽度

为了调整输出,可以通过在流中放入setw操纵符或调用width成员函数,为每项指定输出宽度。下面的例子在一列中以至少10个字符的宽度按右对齐方式输出数值。少于10个的加入引导空格。也可以用cout.fill('*);来设置引导字符。
int main() {
    double arr[] = {1.23, 35.36, 653.7, 4358.24};
    for(int i=0;i<4;i++){
        cout.width(10);
        cout.fill('*');//空格以该字符填充
        cout << arr[i] << endl;
    }
    return 0;
}

如果要为同一行中输出的不同数据项指定不同宽度,也可以使用setw操纵符。并同时设置左或右对齐。
#include <iomanip>
int main() {
    double arr[] = {1.23, 35.36, 653.7, 4358.24};
    string names[] = {"Mary", "Jan", "Hardeson", "Stand"};
    for(int i=0;i<4;i++){
        cout << setiosflags(ios_base::left) << setw(10)  << arr[i]
             << setiosflags(ios_base::right) << setw(20) << names[i]
             << endl << resetiosflags(ios::right);
    }
    return 0;
}

*/
/*
控制符

控制符							作用
setfill						设置填充字符
setprecision				设置浮点数精为n,在以fixed和scientific方式输出时,n为小数位数
setw(n)						设置字段宽度为n
setiosflags(ios:left)		输出左对齐
setiosflags(ios:right)		输出右对齐
setiosflags(ios:uppercase)	数据以十六进制形式输出时字母以大写表示
setiosflags(ios:showpos)	输出正数时带+号


cout << resetiosflags(ios::basefield);
cout << setiosflags(ios::hex) << setiosflags(ios::uppercase) << a << endl;
也可以用
cout << hex << a << endl;
cout << dec ;
或
cout.setf(ios::dec);




ios类静态属性

属性					作用
ios::left			输出数据在本域宽范围内向左对齐
ios::right			输出数据在本域宽范围内向右对齐
ios::internal		数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充
ios::showbase		强制输出整数的基数(八进制数以0打头,十六进制数以0x打头)
ios::showpoint		强制输出浮点数的小点和尾数0
ios::uppercase		在以科学记数法格式E和以十六进制输出字母时以大写表示
ios::showpos		对正数显示“+”号
ios::scientific		浮点数以科学记数法格式输出
ios::fixed			浮点数以定点格式(小数形式)输出
ios::unitbuf		每次输出之后刷新所有的流
ios::stdio			每次输出之后清除stdout, stderr

ios_base& unitbuf (ios_base& str);
Flush buffer after insertions
Sets the unitbuf "format" flag for the str stream.
When the unitbuf flag is set, the associated buffer is flushed after each insertion operation.
This flag can be unset with the nounitbuf manipulator, not forcing flushes after every insertion.
For standard streams, the unitbuf flag is not set on initialization.



cout成员函数和控制符的替换

流成员函数			与之作用相同的控制符			作用
precision(n)		setprecision(n)			设置精度为n
width(n)			setw(n)					设置数据输出宽度为n位
fill(c)				setfill(c)				设置填充字符
setf()				setiosflags()			设置输出格式状态,括号中应给出格式状态,内容与控制符setiosflags括号中的内容相同
unsetf()			resetioflags()			终止已设置的输出格式状态,在括号中应指定内容

浮点数输出精度的默认值是6,例如,数3466.9768显示为3466.98。为了改变精度,可以使用setpresion操纵符。此外,还有两个标志会改变浮点数的输出格式,即ios::fixed和ios::scientific。如果设置了ios::fixed,该数输出3466.976800;如果设置为base::scientific,该数输出为3.466977e+003。
设置了ios::fixed或ios::scientific,则精度值确定了小数点后的小数位数。如果都未设置,则精度表明总有效位(除小数点外的数字个数)
*/

2 文件输入输出

/*
由于文件设备并不像显示器与键盘那样是标准的默认设备,并且磁盘文件数量众多,名字未知,它在fstream.h头文件中,并没有像cout, cin那样的预先定义的全局对象。所以,当我们需要处理某个文件时,必须自己定义一个该类的对象。
ofstream类支持文件输出。如果你需要一个只输出的磁盘文件,可以构造一个ofstream类的对象。在打开文件之前或之后可以指定ofstream对象接收二进制或文本数据。


打开文件,表示为文件流对象和指定的磁盘文件建立关联,并指定工作方式(读还是写,是ASCII码文件还是二进制文件)。
输入或输出,针对的内存。输入表示从磁盘读内容到内存中,输出表示从内存写入磁盘。
ofstream myFile;
myFile.open("filename", ios::out);

其中filename是文件路径,如果缺少路径,只含文件名,则默认为当前目录下的文件。
另外ofstream类声明了有参构造方法,我们可以像下面这样在声明文件流对象时一并打开文件,比较方便:
ofstream myFile("filename", ios::out);
*/
/*
输入/输出方式是在ios类中定义的,它们是枚举常量,有多种选择,如下表:
方式						作用
ios::in				以输入方式打开文件
ios::out				以输出方式打开文件(默认方式),如果文件已存在,则清除其内容
ios::app			以输出方式打开文件,写入的数据添加在文件末尾
ios::ate			打开一个已有的文件,文件指针指向文件末尾
ios::trunc			打开一个文件,如果文件已存在,则清除其全部内容。如果文件不存在,则建立新文件。如已指定了ios::out方式,而未指定								ios::app,ios:ate,iso::in,则同时默认此方式

ios::binary			以二进制方式打开一个文件,如不指定此方式,则默认为ASCII方式
ios::nocreate		打开一个已有的文件,如果文件不存在,则打开失败。nocreate的意思是不建立新文件
ios::noreplace		如果文件不存在,则建立文件。如果存在,则打开失败。replace意思是不更新原不文件
ios::in|ios::out	以输入和输出方式打开,文件可读可写
ios::out|ios:binary	以二制制方式打开一个输出文件
ios::in|ios::binary	以二制制方式打开一个输入文件




几点说明:
1 新版本I/O库中不提供ios::nocreate 和noreplace
2 每一个打开的文件都有一个文件指针,该指针的初始位置由打开方式决定,每读写一个字节,指针向后移一个字节,当文件指针移到最后,就会遇到文件结束EOF(其值为-1),此时流对象的成员函数eof的值为非零值(一般为1),表示文件结束了。

3 可以使用位或运算符对打开方式进行组合,如
ios::in|ios::out|ios:binary  表示以可读可写方式打开二进制文件,但不能组合相互排斥的方式。

4 如果打开操作失败,open函数返回值为0,如果是通过调用构造函数方式打开文件,则流对象的值为0,可以据此判断打开是否成功。
5 在对文件的读写完成之后,要关闭文件,方法是 myFile.close();
关闭文件就是解决文件流对象和磁盘文件的关联,原来设置的工作方式也会失效。这样就不能再通过文件流对象对该文件进行操作。此时,可以能过, myFile.open("filename2", "mode") 将文件流对象与其它文件再建立关联,进行读写操作。




判断文件是否存在

ifstream fin("a.txt", ios::in);
if(fin){
    cout << "1" << endl;
}else{
    cout << "0" << endl;
}




文件的读写

ifstream的成员函数

char get() 读入一个字符并返回(包括回车;tab;空格等空白字符)
char ch; 
ch=cin.get();
istream& get(char&) 读入一个字符,读取成功返回非0,读取失败0
istream& get(char *p, int n,char c);。
istream& getline(char *p, int n, char c);
从输入流中读取n-1字符,或包括终止字符在内的行放入缓冲区,然后把除终止字符外的内容赋给字符数组或字符指针所指向的空间,当第三个参数省略时,系统默认为'\0'。
区别:
get, 不取走缓冲区的终止字符,将导致缓冲区非空。如果是换行,下次get仍然读缓冲区的换行符,无法继续读文件。
getline,取走缓冲区的终止字符,结果是缓冲区为空。下次getline可继续读文件其余内容。

vi显示中文乱码的问题:
$ vi ~/.vimrc
let &termencoding=&encoding
set fileencodings=utf-8,gbk
$ :wq




读写指针问题

在读写文件时,有时希望直接跳到文件中的某处开始读写,这就需要先将文件的读写指针指向该处,然后再进行读写。
ifstream 类和 fstream 类有 seekg 成员函数,可以设置文件读指针的位置;
ofstream 类和 fstream 类有 seekp 成员函数,可以设置文件写指针的位置。

所谓“位置”,就是指距离文件开头有多少个字节。文件开头的位置是 0。

这两个函数的原型如下:
ostream & seekp (int offset, int mode);
istream & seekg (int offset, int mode);

mode 代表文件读写指针的设置模式,有以下三种选项:
ios::beg:让文件读指针(或写指针)指向从文件开始向后的 offset 字节处。offset 等于 0 即代表文件开头。在此情况下,offset 只能是非负数。
ios::cur:在此情况下,offset 为负数则表示将读指针(或写指针)从当前位置朝文件开头方向移动 offset 字节,为正数则表示将读指针(或写指针)从当前位置朝文件尾部移动 offset字节,为 0 则不移动。
ios::end:让文件读指针(或写指针)指向从文件结尾往前的 |offset|(offset 的绝对值)字节处。在此情况下,offset 只能是 0 或者负数。

此外,我们还可以得到当前读写指针的具体位置:
ifstream 类和 fstream 类还有 tellg 成员函数,能够返回文件读指针的位置;
ofstream 类和 fstream 类还有 tellp 成员函数,能够返回文件写指针的位置。

这两个成员函数的原型如下:
int tellg();
int tellp();





*/

ASCII文件

/*
要获取文件长度,可以用 seekg 函数将文件读指针定位到文件尾部,再用 tellg 函数获取文件读指针的位置,此位置即为文件长度。
ASCII文件读写

ASCII文件是文本文件的一种,其中存储的是ASCII字符,不含中文等其它国家文字。
如果文件中,一个字节存放一个字符,这个文件就是ASCII文件。
int main(){
    ofstream fout("text.txt", ios::out);
    if(!fout){
        cout << "open fail!" << endl;
        return -1;
    }
    fout << "I love c++!" << endl;
    fout << "中国" << endl;
    fout.close();

    ifstream fin("text.txt", ios::in);

    char buf[100];
    while(!fin.eof()){
        fin.getline(buf, 100);
        cout << buf << endl;
    }
    fin.close();
    return 0;
}

或者:
int main(){
   char buf[100] = "hello world";
    fstream file("test.txt", ios::out | ios::in);
    if (!file) {
        cout << "open fail!" << endl;
        return -1;
    }

    file.seekg(0, ios::end);
    cout << "len:" << file.tellg() << endl;
    file.seekg(0, ios::beg);

    file << buf; // 将buf里的内容写入文件
    file >> buf; // 将文件的内容写入buf
    cout << buf << endl;

    file.close();
}
*/

二进制文件

/*
二进制文件读写

它将内存中的数据存储形式不加转换的传递到磁盘文件,因此又称为内存数据的映像文件。因此,文件中的信息不是字符数据,而是二进制形式的信息。
对二进制文件的读写主要用类istream中的read和ostream中的write来实现。这两个函数的原型为:
istream& read(char* buffer, int len);
ostream& write(const char* buffer, int len);
写入:
class Student {
    int age;
    char name[10];
public:
    Student(int age, const char* name){
        this->age = age;
        strcpy(this->name, name);
    }
    void show(){
        printf("Student[age=%d,name=%s]\n", age, name);
    }
};

int main(){
    Student s(20, "zhangsan");

    ofstream fout("stu.info", ios::out|ios::binary);
    if(!fout){
        cout << "open fail!" << endl;
        return -1;
    }
    fout.write((char*)&s, sizeof(Student));
    fout.close();

    return 0;
}

读出:
class Student {
    int age;
    char name[10];
public:
    Student(){}
    Student(int age, const char* name){
        this->age = age;
        strcpy(this->name, name);
    }
    void show(){
        printf("Student[age=%d,name=%s]\n", age, name);
    }
};

int main(){
    Student s;

    ifstream fin("stu.info", ios::in|ios::binary);
    if(!fin){
        cout << "open fail!" << endl;
        return -1;
    }
    fin.read((char*)&s, sizeof(Student));
    fin.close();
    s.show();
    return 0;
}


1 Windows下的24bit位图默认偏移量是54字节
2 存储采用小端模式,低8位存B, 中8位存G,高8位存R
3 图像中像素的存储顺序为从左到右,从下到上。如上图中,绿->黄->红->紫 
4 photoshop中保存位图时可以设置翻转行序,即存储顺序为从左到右,从上到下。如上图,红->紫->绿->黄
5 windows下每一行像存储空间必须为4字节的整数倍。如上图中,每行补00 00
6 Phtoshop在文件的末尾还补充了两个0字节,好像是要整体补位。其它软件生成的BMP文件也没有整体补位的,这看起来像是Adobe的独创,不知道目的何在。




*/

字符串读写

/*
字符串流的读写

就是将文件流与字符串或字符数组关联起来,马字符串或字符数组当做文件一样来操作。而字符串实际是在内存中的。
当输入时,将从流对象的字符数组中,读出数据到内存;输出时,将内存中的数据写入流对应的字符数组中。
例子:
#include <strstream>

using namespace std;

class Student {
public:
    int age;
    char name[10];
    Student(int age, const char* name){
        this->age = age;
        strcpy(this->name, name);
    }
    void show(){
        printf("Student[age=%d,name=%s]\n", age, name);
    }
};

int main(){
    Student s(20, "zhangsan");

    char buf[50];

    ostrstream fout(buf, 50);
    if(!fout){
        cout << "open fail!" << endl;
        return -1;
    }
    fout << s.age << "," << s.name << ends; //\0
    cout << buf << endl;

    return 0;
}

中定义了strstream、istrstream、ostrstream等类,这些类已经不推荐使用。
另一个例子:
#include <ssstream>
int main(){
    string s = "20 zhangsan male";
    stringstream ss;
    ss << s;

    int age;
    string name;
    string sex;

    ss >> age >> name >> sex;
    cout << "age:" << age << ", name:" << name << ", sex:" << sex << endl;
}
*/

十四 lambda表达式

/*
C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。

Lambda 的语法形式如下:[capture](parameters) mutable ->return-type{statement}
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}。

可以看到,Lambda 主要分为五个部分:[捕获]、(函数参数)、mutable 或 exception 声明、-> 返回值类型、{函数体}

(1) [capture]:标识一个 Lambda 表达式的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this,通俗:主函数的局部变量等)。函数对象参数有以下形式:
    1 []。没有任何函数对象参数。
    2 [=]。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
    3 [&]。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
    4 [this]。函数体内可以使用 Lambda 所在类中的成员变量。
    5 [a]。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
    6 [&a]。将 a 按引用进行传递。
    7 [a,&b]。将 a 按值传递,b 按引用进行传递。
    8 [=,&a,&b]。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
    9 [&,a,b]。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
    注意:值传递,在lambda函数定义时就确定了,不会随lambda引用改变。

(2) (parameters) 与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递。

(3) mutable 或 exception 声明:这部分可以省略。按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。

(4) ->return-type-> 返回值类型:标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

(5) {statement}{函数体}:标识函数的实现,这部分不能省略,但函数体可以为空。
*/

举例

#include "plural.hpp"
#include <iostream>
using namespace std;

int main(int argc, char const *argv[]) {

    int j = 10;
    auto val = [=] { return j + 1; };
    auto ref = [&] { return j + 1; };

    cout << "val:" << val() << endl; // 11  值传递,返回10+1
    cout << "ref:" << ref() << endl; // 11	引用传递,返回10+1

    j++;
    cout << "val:" << val() << endl; // 11	此处j为10,j的值为lamda表达式定义时,j的值,之后改变不影响表达式内部的值
    cout << "ref:" << ref() << endl; // 12	引用传递,返回11+1

    return 0;
}

2

/*
不同编译器的具体实现可以有所不同,但期望的结果是: 按引用捕获的任何变量,Lambda 函数实际存储的应该是这些变量在创建这个 Lambda 函数的函数的栈指针,而不是 Lambda 函数本身栈变量的引用。不管怎样,因为大多数 Lambda 函数都很小且在局部作用中,与候选的内联函数很类似,所以按引用捕获的那些变量不需要额外的存储空间。
如果一个闭包含有局部变量的引用,在超出创建它的作用域之外的地方被使用的话,这种行为是未定义的!
Lambda 函数是一个依赖于实现的函数对象类型,这个类型的名字只有编译器知道. 如果用户想把 lambda 函数做为一个参数来传递, 那么形参的类型必须是模板类型或者必须能创建一个 std::function 类似的对象去捕获 lambda 函数.使用 auto 关键字可以帮助存储 lambda 函数,
auto my_lambda_func = [&](int x) { …  };
auto my_onheap_lambda_func = new auto([=](int x) {  …  });

一个没有指定任何捕获的 lambda 函数,可以显式转换成一个具有相同声明形式函数指针.所以,像下面这样做是合法的:
auto a_lambda_func = [](int x) {  …  };
void (*func_ptr)(int) = a_lambda_func;
func_ptr(4); // calls the lambda
*/


/*
关于mutable:在值传递时,不加mutable,不能修改传进去的值
						加了mutable,只在函数体里改变其值,外部不变
			引用传递时,加与不加,都可修改其自身的值。
*/

3 类型推导

/*
C++98:
	一般用到模板中,不需要了解细节
C++11:
    auto
    万能引用
    lambda捕获
    lambda返回值
    decltype
C++14:
    函数返回值
    有初始值的lambda捕获
    
    
    
    
typeid和decltype
typeid(x).name() 可以返回 x的数据类型对应的字符串
decltype(x) 仅仅查询的 x类型,不会返回数据类型,即不能打印,但可以作为类型使用
decltype跟auto和模板推断不同,它将返回精确的类型(auto和模板推断在某些情况下会去掉 const和引用)

比如:
cout << typeid(1.5).name() ; //输出double
cout << decltype(1.5) ; //报错
decltype(1.5) a = 2.3 ; //正确,decltype(1.5) 是double类型

typeid(expr).name() 方法会去除类型的引用并且其结果可读性差,在gcc下不够直观。为了方便建议采用boost库中提供的
boost::typeindex::type_id_with_cvr (保留 const, volatile和引用信息)
用法:
#include <boot/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

cout << type_id_with_cvr<T>().pretty_name()  << endl;
cout << type_id_with_cvr<decltype(temp)>().pretty_name()  << endl;



模板类型推导
template<typename T>
void f(ParamType param);
f(expr);  //编译器依据此时传递的实参,进行推导,得出T的实际类型,进而生成函数模板

由于函数参数类型可能为T的值,引用,指针,或const修饰等,所以ParamType可能是这样:
template<typename T>
void f(const T& param);  //ParamType is const T&
int x=0;
f(x);   //call f with an int



对于函数调用 f(x),T 会被推导为 int,而 ParamType 则被推导为 const int&。

很容易误以为 T 与实参 x 的类型肯定相同,特别是在上面这个例子里它们都为 int。然而这只是凑巧罢了。T 的类型推导不仅依赖于 expr 的类型,还取决于 ParamType。ParamType 取三种不同的类型时,T 的类型推导过程都不同:


1. ParamType 是指针或引用类型,但不是通用引用(universal reference),如 const T&、T*。
模板参数类型为引用的例子:
template<typename T>
void f(T& param){  //param is a reference
    cout << typeid(decltype(param)).name() << endl;
}

int x = 27; // x is an int
const int cx = x; //cx is const int
const int& rx = x; //rx is a reference to x as a const int

f(x); //T is int, param's type is int&
f(cx); //T is const int, param's type is const int&
f(rx); //T is const int, param's type is const int&


template <typename T>
void f(T *param) {
    ECHO()
}
int main() {
    int a = 10;
    int *p = &a;
    const int *q = &a;
    f(p);
    f(q);
    return 0;
}
------------------------------
T          = int
param      = int*
------------------------------
T          = int const
param      = int const*




2. ParamType 是通用引用类型,如 T&& param。

如果 expr 是左值(lvalue),则 T 和 ParamType 都被推导为左值引用。注意,仅在这种情况 T 才会被推导为引用类型。另外,尽管形参的定义是 T&& param,但 param 本身是一个参数变量,因此它的类型是左值而不是右值。所有的形参都是左值类型。
如果 expr 是右值(rvalue),按之前论述的第一种情形处理(即形参是指针或引用类型时的处理方式)。

template<typename T>
void f(T&& param){
    ECHO();
}
int main(){
    int x = 27;
    const int cx = x;
    const int& rx = x;

    f(x);   // x is lvalue, so T is int&, param's type is also int&
    f(cx);  // cx is lvalue, so T is const int&, param's type is also const int&
    f(rx);  // rx is lvalue, so T is const int&, param's type is also const int&
    f(27);  // 27 is rvalue, so T is int, param's type is therefore int&&
}
------------------------------
T          = int&
param      = int&
------------------------------
T          = int const&
param      = int const&
------------------------------
T          = int const&
param      = int const&
------------------------------
T          = int
param      = int&&




3. ParamType 不是指针也不是引用,如 T param。

当 ParamType 既不是指针也不是引用,我们便在传值(pass-by-value):
template<typename T>
void f(T param);    // param is now passed by value
这意味着 param 是实参的一份拷贝,一个新的对象。因此工作过程为:
像之前一样,
如果 expr 的类型是引用,则忽略引用部分。
如果 expr 的类型含const,则把 const 也忽略。
如果 expr 的类型含volatile,则也忽略 volatile。

template<typename T>
void f(T param){
    ECHO();
}
int main(){
    int x = 27;
    const int cx = x;
    const int& rx = x;
    f(x);   // T's and param's type are both int
    f(cx);  // T's and param's type are again both int
    f(rx);  // T's and param's type are still both int
}

------------------------------
T          = int
param      = int
------------------------------
T          = int
param      = int
------------------------------
T          = int
param      = int


*/

auto

/*
C++11 为了顺应这种趋势也开始支持自动类型推导了!C++11 使用 auto 关键字来支持自动类型推导。

注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
简单用法
#include "stdio.h"
#include "typetest.h"

template<typename T>
void f(T param){
    ECHO();
}

int main(){
    auto n = 10;
    auto f = 12.8;
    auto p = &n;
    auto url = "http://note.mmyf.cn";

    type(n);
    type(f);
    type(p);
    type(url);
}
T = int
T = double
T = int*
T = char const*


auto 的高级用法

auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型。请看下面的代码:
int  x = 0;
auto *p1 = &x;   //p1 为 int *,auto 推导为 int
auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
auto &r1  = x;   //r1 为 int&,auto 推导为 int
auto r2 = r1;    //r2 为  int,auto 推导为 int

auto 的限制
前面介绍推导规则的时候我们说过,使用 auto 的时候必须对变量进行初始化,这是 auto 的限制之一。那么,除此以外,auto 还有哪些其它的限制呢?
auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中。
auto 关键字不能定义数组,比如下面的例子就是错误的:
char url[] = "http://c.biancheng.net/";
auto  str[] = url;  //arr 为数组,所以不能使用 auto
auto 不能作用于模板参数,请看下面的例子:
A<int> C1;
A<auto> C2 = C1;  //错误
*/

decltype

/*
C++11 新增 decltype 关键字,用来在编译时推导出一个表达式的类型。定义如下:
decltype(exp)  // exp 为一个表达式
decltype 的推导过程是在编译期完成的,并且不会真正计算表达式的值。
int x1 = 0;
decltype(x1) x2 = 1;  // x2 -> int
decltype(x1 + x2) x3 = 0;  // x3 -> int

const int& y1 = x1;
decltype(y1) y2 = y1;  // y2 -> const int&

const decltype(x3)* z1 = &x3;  // z1 -> const int*
decltype(x3)* z2 = &x3;  // z2 -> int*
decltype(z1)* z3 = &z2;  // z3 -> const int**
decltype 精确地推导出表达式定义本身的类型,不会像 auto 那样在某些情况下舍弃引用和 cv 限定符。
推导规则
exp 是标识符、类访问表达式,decltype(exp) 和 exp 的类型一致
class TestA
{
public:
    static const int x_ = 6;
    int y_;
};

int n1 = 6;
volatile const int& n2 = n1;

decltype(n1) x1 = n1;  // x1 -> int
decltype(n2) x2 = n1;  // x2 -> const valatile int&

decltype(TestA::x_) y1 = 6;  // y1 -> const int
// 类访问表达式
TestA a;
decltype(a.y_) y2 = 6;  // y2 -> int
exp 是函数调用,decltype(exp) 和函数返回值一致
int& func_int_l();  // 左值
int&& func_int_r();  // 右值
int func_int();  // 纯右值

const int& func_cint_l();  // 左值
const int&& func_cint_r();  // 右值
const int func_cint();  // 纯右值

const TestA func_ct();  // 纯右值

int n = 6;

decltype(func_int_l()) x1 = n;  // x1 -> int&
decltype(func_int_r()) y1 = 6;  // y1 -> int&&
decltype(func_int()) z1 = 6;  // z1 -> int

decltype(func_cint_l()) x2 = n;  // x2 -> const int&
decltype(func_cint_r()) y2 = 6;  // y2 -> const int&&
decltype(func_cint()) z2 = 6;  // z2 -> int

decltype(func_ct()) a = TestA();  // a -> const TestA
若 exp 是一个左值,则 decltype(exp) 是 exp 的一个左值引用,否则和 exp 的类型一致
// 带括号的表达式和加法运算表达式
const TestA a = TestA();

decltype(a.y_) x1 = 0;  // x1 -> int  根据规则1
decltype((a.y_)) x2 = x1;  // x2 -> const int&  加括号后也为左值,根据规则3,a 是 const,所以 a.y_ 是 const int

int i = 0, j = 0;
decltype(i + j) y1 = 0;  // y1 -> int  i + j 为右值,根据规则3
decltype(i += j) y2 = y1;  // y2 -> int&  i += j 为左值,根据规则3


当声明为指针时:auto 的推导结果将保持初始化表达式的cv属性
当声明为引用时:忽略引用,忽略cv属性
*/

4 函数包装器

/*
传递给算法的“函数型实参”不一定得是函数,可以是行为类似函数的对象。这种对象称为函数对象(function object),或称为仿函数(functor)。——《STL标准库(第2版)》 P233
1. 函数对象 = 仿函数。并且,function object = functor
2. 函数对象(仿函数)有四种实现方式:函数指针(fucntion pointer)、lambda表达式、“带有成员函数 operator()”的class建立的object、“带有转换函数可以将自己转换为 pointer to function”的class所建立的object。

*/
#include "typetest.hpp"
#include <cstdio>
#include <functional>
using namespace std;

class Test {
public:
    void operator()(int n) {
        cout << n << endl;
    }
    static void print() {
        printf("test\n");
    }
};

void print() {
    cout << "hello" << endl;
}
int main() {
    // 封装外部函数
    function<void()> fp(print);
    fp();

    // 封装lamda表达式
    auto p = [](int a, int b) {
        return a + b;
    };
    function<int(int, int)> fp2(p);
    cout << fp2(2, 3) << endl;

    // 重写()运算符
    Test t;
    t(2);

    function<void(int)> fp3(t);
    fp3(2);

    // 封装类中static成员函数
    auto p2 = Test::print;
    function<void(void)> fp4(p2);
    fp4();

    return 0;
}

举例

#include "stdio.h"
#include "typetest.h"
#include "math.h"
using namespace std;
template <typename T, typename F>
T use_f(T v, F f) {
    static int count = 0;
    count++;
    cout << " use_f: count=" << count << ", &count=" << &count << endl;
    return f(v);
}

class Fq {
    private:
    double z_;

    public:
    Fq(double z = 1.0) : z_(z) {}
    double operator()(double q) { return z_ + q; }
};

double square(double x) { return x * x; }

int main() {
    double y = 1.21;
    
    cout << use_f(y, square) << endl;
    cout << use_f(y, Fq(5.0)) << endl;
    cout << use_f(y, [](double u) { return sqrt(u); }) << endl;

    return 0;
}

/*输出
 use_f: count=1, &count=0x55bbb184715c
1.4641
 use_f: count=1, &count=0x55bbb1847160
6.21
 use_f: count=1, &count=0x55bbb1847158
1.1
*/

/*

从运行结果中,我们可以看出,在这三次调用中use_f的实例化了3次。使用模板函数,看似统一了操作形式,但其对于不同类型的F对模板函数都要进行一次实例化,这大大增加了编译的时长,并使头文件也增大,同时也降低了代码的执行效率。为了解决这类问题,我们首先能想到的解决办法就是:降低use_f的实例化的次数,理想的情况下是:在这3次循环调用的时候,调用同一个use_f的实例。
针对例子中的函数指针、函数对象和lambda表达式,它们有一个共同的特征:都是接受一个double参数并返回一个double值。也就是它们的调用特征标(它们的特征标都是double(double))相同。这便是function解决这个问题的关键。【注:调用特征标是由返回类型和参数类型列表决定的,其格式为:返回类型(参数类型列表),其中每个参数类型用逗号分隔。】
因此,C++11引入了function包装器。function包装器可以简单理解为一个接口,它可以将特征标相同的函数指针、函数对象和lambda表达式等统一定义为一类特殊的对象。
现在回到我们最开始的问题,我们使用function包装器将它们将统一“包装”成function<double(double)类型,这样模板函数use_f将只实例化一次。使用function包装器改进后的代码如下所示:
*/

//改进后
#include <functional>
#include <iostream>
#include <math.h>
using namespace std;
template <typename T, typename F>
T use_f(T v, F f) {
    static int count = 0;
    count++;
    std::cout << "use_f count = " << count << ", &count = " << &count
              << std::endl;
    return f(v);
}

class Fq {
private:
    double z_;

public:
    Fq(double z = 1.0) : z_(z) {}
    double operator()(double q) { return z_ + q; }
};

double square(double x) { return x * x; }

int main() {

    double y = 1.21;
    typedef function<double(double)> fdd; // simplify the type declaration

    cout << use_f(y, fdd(square)) << endl;
    cout << use_f(y, fdd(Fq(5.0))) << endl;
    cout << use_f(y, fdd([](double u) { return sqrt(u); })) << endl;

    return 0;
}

/*输出
use_f count = 1, &count = 0x55d2cffc2154
1.4641
use_f count = 2, &count = 0x55d2cffc2154
6.21
use_f count = 3, &count = 0x55d2cffc2154
1.1
*/


/*
从输出结果可以看出,use_f确实只实例化了一次,增加了编码效率,3次循环调用同一个函数,增加了代码额执行效率。
总结
function包装器将可调用对象的类型进行统一,便于我们对其进行统一化管理,同时,使用function包装器可以解决模板效率低下,实例化多份的问题。
*/

十五 STL标准模板库

/*
STL最初是HP公司开发的一个用于支持C++泛型编程的模板库,于1994年被纳入C++标准,成为C++标准库的一部分。由于C++标准库有多种不同的实现,因此STL也有不同的版本,但它们为用户提供的接口都是遵循共同标准的。
根本上讲,STL就是一些容器,例如list, vector, set, map等,以及一些算法和其它组件的集合。STL的目的是标准化,这样就不用重复开发,在开发项目的时候,可以直接使用现成的组件。
STL提供了常用的数据结构和算法。例如vector就是STL中提供的一个向量容器,各种排序,查找算法等。
排序的函数模板内在要求

从函数模板的声明来看,类型参数T可以是任意类型,但实际情况并非如此,类型T必须具备3个特点:
1) 类型T的变量之间能够比较大小
2) 类型T必须具有公有的复制构造函数
3) 数型T的变量之间能够用“=”赋值
并非所有类型都具有这3个能力,例如一个没有重载 “<”,“>”等运算符就不具体比较大小的功能,复制构造函数私有的类就不具体第二个功能,静态数组类型就不具备第三个功能(无法用“=”给整个数组赋值)
静态数组不是使用static 关键字修饰的数组,而是普通的数组,为了与后面的动态数组分离开,才说明它是静态数组。
我们可以把具体上述三个特性的类型,称为具体一个概念,可以把这个概念记做Sortable。具备某个概念的类型,称为这一概念的模型(Model)。
泛型程序设计及STL的结构

有效的利用已有的成果,将经典的,优秀的算法标准化,模块化,从而提高软件的生产效率,是软件产业化的需求。为实现这一需求,不仅需要面向对象的程序设计思想,也需要泛型程序设计思想。C++语言提供的标准模板库便是面向对象程序设计与泛型设计思想相结合的一个良好典范。
所谓泛型程序设计,就是编写不依赖于具体数据类型的程序。C++中,模板是泛型设计的主要工具
标准C++库

在C语言中,系统函数,系统的外部变量和一些宏定义都放置在运行库中。C++的库中除了继续保留了大部分C语言系统函数外,还加入了预定义的模板和类。
STL现在是C++的一部分,因此不用再额外的库文件。
标准C++类库是一个极为灵活并可扩展,可重用的软件模块的集合。
在使用标准C++库时,需要在源文件中使用 using namespace std; 该语句不宜放在头文件中,因为这会使一个命名空间不被察觉的对一个源文件开放。
STL的内容

STL可分为容器,迭代器,空间配置器,适配器,算法,仿函数六大组件。
在C++标准中,STL被组织为17个头文件:
algorithm 	deque 		functional 		iterator 	array 
vector 		list 		forward_list 	map 		unordered_map 
memory 		numeric 	queue 			set 		unordered_set 
stack 		utility

*/

1 容器


/*
容器

通过设计了一些模板类,STL容器对最常用的数据结构提供了支持,这些模板的参数允许我们指定容器中元素的数据类型。
容器就是将我们常用的数据结构,如变长数组,链表,栈,队列,二叉树等封装成一个个模板类,以方便使用。
常见的数据结构和头文件的对应关系

算法

STL提供了大约100个常用算法的模板函数,主要由头文件algorithm, numeric 和functional组成。常用的功能涉及到比较,交换,查找,遍历,复制,修改,移除,反转,合并等。
迭代器

软件设计中有一个基本原则,即所有的问题都可以通过引进一间接层来简化,这种简化在STL中就是用迭代器来完成的。概括来说,迭代器在STL中用来将算法和容器联系起来。
几乎STL提供的所有算法都是通过迭代器存取元素序列进行工作的。每一个容器都定义了其本身专有的迭代器,用以存取容器中的元素。
迭代器部分主要由头文件utility,iterator和memory组成。
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WsVCz33-1678436419475)(D:\1aaaasuqian\day\img\image-20230309100757428.png)]

vector

#include <cstdio>
#include <iostream>
#include <vector>

using namespace std;
void printV(vector<int> &v) {
    vector<int>::iterator it_s = v.begin();
    vector<int>::iterator it_e = v.end();
    while (it_s != it_e) {
        printf("%d ", *it_s++);
    }
    printf("\n");
}

int main() {
    vector<int> a;
    printf("a.size=%d\n", a.size());
    a.push_back(1);
    a.push_back(2);
    printf("a.size=%d\n", a.size());

    vector<int> b(10);
    printf("b.size=%d\n", b.size());
    b.push_back(18);
    b.push_back(20);
    printf("b.size=%d\n", b.size());

    for (int i = 0; i < b.size() - 2; i++) {
        b[i] = i;
    }
    printV(b);

    vector<int> c(10, 3);
    printV(c);

    vector<int> d = b;
    printV(d);

    d = c;
    printV(d);

    vector<int> e(b.begin() + 3, b.end());
    printV(e);

    b.resize(20);
    printf("b.size=%d\n", b.size());
    printV(b);

    b.resize(8);
    printf("b.size=%d\n", b.size());
    printV(b);

    b.erase(b.begin() + 2, b.begin() + 4); // 最后一个不删除
    printV(b);

    b.insert(b.begin() + 2, b.begin(), b.end());
    printV(b);

    return 0;
}

deque

/*
deque与vector非常相似。它也采用动态数组管理元素,提供随机存取,有着和vector几乎一样的接口。不同的是deque的动态数组头尾都开放,因此能在头尾两端进行快速安插和删除。
deque与vector的主要不同之处在于:
两端都能快速插入和删除元素,这些操作可以在常数时间(amortized constant time)内完成。
元素的存取和迭代器的动作比vector稍慢。
迭代器需要在不同区块间跳转,所以它非一般指针。
deque与vector组织内存的方式不一样。在底层,deque按“页”(page)或“块”(chunk)来分配存储器,每页包含固定数目的元素。而vector只分配一块连续的内存。例如,一个10M字节的vector使用的是一整块10M字节的内存,而deque可以使用一串更小的内存块,比如10块1M的内存。所以不能将deque的地址(如&deque[0])传递给传统的C API,因为deque内部所使用的内存不一定会连续。

优先使用vector,还是deque?
c++标准建议:vector是那种应该在默认情况下使用的序列。如果大多数插入和删除操作发生在序列的头部或尾部时,应该选用deque。
*/
#include "plural.hpp"
#include <cstdio>
#include <deque>
#include <iostream>
#include <vector>
using namespace std;

void printDeque(deque<Plural> de) {
    auto beg = de.begin();
    auto end = de.end();
    while (beg < end) {
        cout << *beg << " ";
        beg++;
    }
    cout << endl;
}

int main() {

    deque<Plural> de;
    de.push_front(Plural(1, 1));
    de.push_front(Plural(2, 2));
    de.push_front(Plural(3, 3));
    de.push_front(Plural(4, 4));

    de.push_back(Plural(0, 0));

    de.pop_back();
    de.pop_front();
    printDeque(de);

    return 0;
}

list

//list是一个双向链表容器,所以在任何位置进行插入或删除,性能都是不错的。不支持随机存取元素,所以不支持[], at(pos)。
#include "plural.hpp"
#include <cstdio>
#include <iostream>
#include <list>
using namespace std;

void printList(auto li) {
    auto beg = li.begin();
    auto end = li.end();
    while (beg != end) {
        cout << *beg << " ";
        beg++;
    }
    cout << endl;
}

int main() {
    list<Plural> li;
    li.push_front(Plural(1, 1));
    li.push_front(Plural(2, 2));
    li.push_front(Plural(3, 3));
    li.push_front(Plural(4, 4));

    li.push_back(Plural(0, 0));

    li.pop_back();
    li.pop_front();
    printList(li);

    return 0;
}

/*
vector, deque, list的选择

如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
如果你需要大量的插入和删除,而不关心随即存取,则应使用list
如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。
*/

map

/*
map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,它完成有可能在我们处理一对一数据的时候,在编程上提供快速通道。这里说下map内部数据的组织,map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的,后边我们会见识到有序的好处。
下面举例说明什么是一对一的数据映射。比如一个班级中,每个学生的学号跟他的姓名就存在着一一映射的关系,这个模型用map可能轻易描述,很明显学号用int描述,姓名用字符串描述(本篇文章中不用char *来描述字符串,而是采用STL中string来描述),
*/
#include "plural.hpp"
#include <cstdio>
#include <iostream>
#include <map>
using namespace std;

void printMap(map<int, string> ma) {
    // auto beg = ma.begin();
    map<int, string>::iterator beg = ma.begin();
    auto end = ma.end();
    while (beg != end) {
        cout << beg->first << " " << beg->second << endl;
        beg++;
    }
    // putchar(10);
}

int main() {
    map<int, string> ma;

    // t不是一个map类型迭代器,是pair型
    // pair<map<int, string>::iterator, bool> t
    auto t = ma.insert(pair(1, "one")); // 插入操作返回一个键值对,第一个变量存放map类型迭代器,第二个变量即.second 存放是否插入成功
    // cout << t.second << endl;
    ma.insert(pair<int, string>(2, "two"));
    ma[3] = "three"; // 可以覆盖之前的键值对
    ma.insert(map<int, string>::value_type(4, "five"));
    ma[4] = "four"; // 可以覆盖之前的键值对
    ma[5] = "five";
    printMap(ma);
    printf("---------------------------\n");

    // 查找   1
    // map<int, string>::iterator t2;
    auto t2 = ma.find(7); // t2为map类型迭代器,find参数为键,->second存放找到的值。若找不到,返回与ma.end()一样的值
    if (t2 == ma.end()) {
        printf("no result\n");
    } else {
        cout << t2->second << endl;
    }

    // 查找    2
    map<int, string>::iterator t3;
    t3 = ma.lower_bound(3);     // 在begin到end-1范围,找第一个大于等于3(参数)的键,返回map类型迭代器
    cout << t3->second << endl; // 前提:需要有序     找不到返回end
    t3 = ma.upper_bound(4);     // 在begin到end-1范围,找第一个大于3(参数)的键,返回map类型迭代器
    cout << t3->second << endl;

    t3 = ma.upper_bound(7);
    if (t3 == ma.end()) {
        printf("no result\n");
    } else {
        cout << t3->second << endl;
    }
    printf("---------------------------\n");

    // 删除
    map<int, string>::iterator t4;
    t4 = ma.find(5);     // 首先使用迭代器找到,直接以迭代器为参数
    ma.erase(t4);        // 此时无返回值
    int k = ma.erase(1); // 当直接以键为参数时,存在返回值,删除了返回1,删除失败返回0

    printMap(ma);

    printf("---------------------------\n");

    return 0;
}

set

/*
set也是关联容器。set作为一个容器也是用来存储同一数据类型的数据,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。
*/
/*
常用API
begin()             ,返回set容器的第一个迭代器
end()             ,返回set容器的最后一个迭代器
clear()             ,删除set容器中的所有的元素
empty()            ,判断set容器是否为空
max_size()           ,返回set容器可能包含的元素最大个数
size()            ,返回当前set容器中的元素个数
rbegin            ,返回的值和end()相同
rend()            ,返回的值和rbegin()相同
erase(iterator)       ,删除定位器iterator指向的值
erase(first,second)   ,删除定位器first和second之间的值
erase(key_value)      ,删除键值key_value的值
*/
#include <cstdio>
#include <iostream>
#include <set>

using namespace std;

void printV(set<int> s) {
    auto beg = s.begin();
    auto end = s.end();
    while (beg != end) {
        cout << *beg << " ";
        beg++;
    }
    putchar(10);
}
int main(int argc, char const *argv[]) {
    set<int> s;
    s.insert(1);
    s.insert(2);
    s.insert(3);
    s.insert(4);
    s.insert(5);
    s.insert(5); // 此句不会生效
    printV(s);
    printf("---------------------------------\n");

    cout << "5的次数:" << s.count(5) << endl;

    pair<set<int>::const_iterator, set<int>::const_iterator> it;
    it = s.equal_range(3); // 此种类型迭代器,前者存放第一个大于等于参数的值
                           // 后者存放第一个大于参数的值
    cout << *it.first << endl;
    cout << *it.second << endl;
    printf("---------------------------------\n");

    // 第一种删除
    s.erase(s.begin());
    printV(s);
    printf("---------------------------------\n");

    // 第二种删除
    auto first = s.begin();
    auto second = s.begin();
    second++;
    second++;
    s.erase(first, second); // 范围删除,左闭右开
    printV(s);
    printf("---------------------------------\n");

    // 第三种删除
    set<int>::const_iterator iter;
    s.erase(8); // 删除8
    cout << "删除后 set 中元素是 :";
    for (iter = s.begin(); iter != s.end(); ++iter) {
        cout << *iter << " ";
    }
    cout << endl;
    printV(s);
    printf("---------------------------------\n");

    return 0;
}

2 算法

/*
accumulate(); //组合所有元素(如总和,求积等)
accumulate(); //组合所有元素(如总和,求积等)

STL 中有以下实现“累加”功能的算法(函数模板):
template <class InIt, class T, class Pred>
T accumulate(Init first, Init last, T val, Pred op);

该模板的功能是对 [first, last) 中的每个迭代器 I 执行 val = op(val, *I),返回最终的 val。在 Dev C++ 中,numeric 头文件中 accumulate 的源代码如下:
template <class Init, class T, class Pred>
T accumulate(Init first, Init last, T init, Pred op)
{
    for (; first != last; ++first)
        init = op(init, *first);
    return init;
};

此模板被实例化后,op(init, *first)必须要有定义,则 op 只能是函数指针或者函数对象。因此调用该 accmulate模板时,形参 op 对应的实参只能是函数名、函数指针或者函数对象。
例程-计算平方和

#include <numeric>

int SumSquares(int total, int value){
    return total + value * value;
}

void printV(vector<int>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size = %d\n", size);
    for(int i=0;i<size;i++){
        printf("%d ", v[i]);
    }
    printf("\n--------------------------------------;\n");
}

int main(){
    const int SIZE = 10;
    int a1[] = { 1,2,3,4,5,6,7,8,9,10 };
    vector<int> v(a1, a1 + SIZE);
    printV(v);
    int result = accumulate(v.begin(), v.end(), 0, SumSquares);
    cout << "平方和:" << result << endl;

    return 0;
}

第四个参数是 SumSquares 函数的名字。函数名字的类型是函数指针,因此本行将 accumulate 模板实例化后得到的模板函数定义如下:
int accumulate(vector <int>::iterator first, vector <int>::iterator last, int init, int(*op)(int, int)) {
    for (; first != last; ++first)
        init = op(init, *first);
    return init;
}

函数对象

如果一个类重载了()运算符,这个类就称为函数对象类,这个类的对象就是函数对象。函数对象是一个对象,但是使用的形式看起来像函数调用,实际上也执行了函数调用,因而得名。
class CAverage {
public:
    double operator()(int a1, int a2, int a3) {
        return (double)(a1 + a2 + a3) / 3;
    }
};

int main() {
    CAverage average;  //能够求三个整数平均数的函数对象
    cout << average(3, 2, 3);  //等价于 cout << average.operator(3, 2, 3);
    return 0;
}

//输出
2.66667

求n次方和 - 函数对象

void printV(vector<int>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size = %d\n", size);
    for(int i=0;i<size;i++){
        printf("%d ", v[i]);
    }
    printf("\n--------------------------------------;\n");
}

template<class T>
class SumPowers {
private:
    int power;
public:
    SumPowers(int p) :power(p) {}
    T operator() (const T& init, const T& value) { //计算 value的power次方,加到init上
        T v = value;
        for (int i = 0; i < power - 1; ++i)
            v = v * value;
        return init + v;
    }
};

int main(){
    const int SIZE = 10;
    int a1[] = { 1,2,3,4,5,6,7,8,9,10 };
    vector<int> v(a1, a1 + SIZE);

    int result = accumulate(v.begin(), v.end(), 0, SumPowers<int>(3));
    cout << "立方和:" << result << endl;

    result = accumulate(v.begin(), v.end(), 0, SumPowers<int>(4));
    cout << "4次方和:" << result;
}

//输出
立方和:3025
4次方和:25333

第四个参数是 SumPowers<int>(3)。SumPowers 是类模板的名字,SumPowers<int> 就是类的名字。类的名字后面跟着构造函数的参数列表,就代表一个临时对象。因此 SumPowers<int>(3) 就是一个 SumPowers<int> 类的临时对象。
编译器在编译此行时,会将 accumulate 模板实例化成以下函数:
int accumulate(vector<int>::iterator first, vector<int>::iterator last, int init, SumPowers<int> op) {
    for (; first != last; ++first)
        init = op(init, *first);
    return init;
}

形参 op 是一个函数对象,而op(init, *first)等价于:
op.operator()(init, *first);
即调用了 SumPowers<int> 类的 operator() 成员函数。
对比 SumPowers 和 SumSquares 可以发现,函数对象的 operator() 成员函数可以根据对象内部的不同状态执行不同操作,而普通函数就无法做到这一点。因此函数对象的功能比普通函数更强大。
for_each

stl中的定义:
template<class InputIt, class UnaryFunction>
constexpr UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
    for (; first != last; ++first) {
        f(*first);
    }
    return f; // implicit move since C++11
}

求vector元素的和

class Sum {
public:
    int total;
    Sum():total(0){}
    void operator() (int a){
        total += a;
    }
};

void printV(set<int>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size=%d\n", size);
    set<int>::iterator it = v.begin();
    for(; it != v.end();it++){
        cout << *it << " ";
    }
    printf("\n--------------------------------------;\n");
}

int square(int total, int value){
    cout << value << endl;
    return total + value * value;
}

int main(){
    vector<int> v;

    for(int i =1; i< 10;i++){
        v.push_back(i);
    }

    Sum sum;
    sum = for_each(v.begin(), v.end(), sum);
    printf("sum=%d\n", sum.total);

    return 0;
}

//输出
sum=45

find

Return value:
Iterator to the first element satisfying the condition or last if no such element is found.
#include<algorithm>
int main() {
    int n1 = 3;
    int n2 = 5;

    vector<int> v{0, 1, 2, 3, 4};

    auto result1 = find(v.begin(), v.end(), n1);
    auto result2 = find(v.begin(), v.end(), n2);

    if (result1 != v.end()) {
        std::cout << "v contains: " << n1 << '\n';
    } else {
        std::cout << "v does not contain: " << n1 << '\n';
    }

    if (result2 != v.end()) {
        std::cout << "v contains: " << n2 << '\n';
    } else {
        std::cout << "v does not contain: " << n2 << '\n';
    }
}

//输出
v contains: 3
v does not contain: 5

find_if

例程 - 函数

bool IfEqualThree(int ret) {
    return ret == 3;
}

int main() {
    vector v{0, 1, 2, 3, 4};

    auto result = find_if(v.begin(), v.end(), IfEqualThree);

    if (result != v.end()) {
        std::cout << "v contains: 3" << '\n';
    } else {
        std::cout << "v does not contain: 3" << '\n';
    }
}

remove

remove(first, last, val); //删除first到last之间所有值为val的元素
remove在STL中的源代码:
template <class ForwardIterator, class T>
ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val) {
    ForwardIterator result = first;
    while (first!=last) {
        if (!(*first == val)) {
            *result = move(*first);
            ++result;
        }
        ++first;
    }
    return result;
}

remove只是通过迭代器的指针向后移动来删除,将没有被删除的元素放在链表的前面,并返回一个指向新的位置的迭代器。由于remove()函数不是vector成员函数,因此不能调整vector容器的长度。(对vector来说)remove()函数并不是真正的删除,要想真正删除元素则可以使用erase()或者resize()函数。
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxqyxENx-1678436419476)(D:\1aaaasuqian\day\img\000204.gif)]

/*
#include <algorithm>

using namespace std;

void printV(vector<char>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size=%d\n", size);
    auto it = v.begin();
    for(; it != v.end();it++){
        cout << *it << " ";
    }
    printf("\n--------------------------------------;\n");
}

int main() {
    vector<char> v;
    v.push_back('q');
    v.push_back('w');
    v.push_back('e');
    v.push_back('r');
    v.push_back('b');
    v.push_back('w');
    v.push_back('w');
    //printV(v);
    //remove(v.begin(), v.end(), 'w');
    printV(v);
    v.erase(remove(v.begin(), v.end(), 'w'), v.end());
    printV(v);

    return 0;
}

//输出
:--------------------------------------
size=7
q w e r b w w 
--------------------------------------;
:--------------------------------------
size=4
q e r b 
--------------------------------------;

remove_if

函数原型:ForwardIterator remove_if (ForwardIterator first, ForwardIterator last,
UnaryPredicate pred);
简单的说就是:remove_if(first, last, pred) 从first到last中将满足条件pred的元素删除
这个函数就是按条件删除元素
remove_if的参数是迭代器,通过迭代器无法得到容器本身,而要删除容器内的元素只能通过容器的成员函数erase来进行,因此remove系列函数无法真正删除元素,只能把要删除的元素移到容器末尾并返回要被删除元素的迭代器,然后通过erase成员函数来真正删除。
void printV(vector<int>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size=%d\n", size);
    auto it = v.begin();
    for(; it != v.end();it++){
        cout << *it << " ";
    }
    printf("\n--------------------------------------;\n");
}
bool IfEqualThree(int ret) {
    return ret == 3;
}

int main() {
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    v.push_back(3);
    printV(v);
    v.erase(remove_if(v.begin(), v.end(), IfEqualThree), v.end());
    printV(v);
}

//输出
:--------------------------------------
size=7
1 2 3 3 4 5 3 
--------------------------------------;
:--------------------------------------
size=4
1 2 4 5 
--------------------------------------;

sort

STL 中的排序模板 sort 能将区间元素排序。
sort 算法的原型如下:
template <class _Randlt, class Pred>
void sort(_Randlt first, _RandIt last, Pred op);

这个版本和第一个版本的差别在于,元素 a、b 比较大小是通过表达式op(a, b)进行的。如果该表达式的值为 true,则 a 比 b 小;如果该表达式的值为 false,也不能认为 b 比 a 小,还要看op(b, a)的值。总之,op 定义了元素比较大小的规则。下面是一个使用 sort 算法的例子。
class Point {
public:
    int x, y;
    Point(int x, int y):x(x),y(y){}
    friend ostream& operator<<(ostream& os, Point& p);
};

ostream& operator<<(ostream& os, Point& p){
    os << "Point[x=" << p.x << ",y=" << p.y << "]";
    return os;
}

void printV(vector<Point>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size=%d\n", size);
    vector<Point>::iterator it = v.begin();
    for(; it != v.end();it++){
        cout << *it << " ";
    }
    printf("\n--------------------------------------;\n");
}

bool cmp(Point p1, Point p2){
    if(p1.x != p2.x){
        return p1.x < p2.x;
    }else{
        return p1.y < p2.y;
    }
}

int main(){
    vector<Point> v;

    v.push_back(Point(1, 3));
    v.push_back(Point(1, 2));
    v.push_back(Point(1, 1));
    v.push_back(Point(2, 1));
    v.push_back(Point(3, 1));
    v.push_back(Point(2, 2));

    printV(v);

    sort(v.begin(), v.end(), cmp);

    printV(v);

    return 0;
}

//输出
:--------------------------------------
size=6
Point[x=1,y=3] Point[x=1,y=2] Point[x=1,y=1] Point[x=2,y=1] Point[x=3,y=1] Point[x=2,y=2] 
--------------------------------------;
:--------------------------------------
size=6
Point[x=1,y=1] Point[x=1,y=2] Point[x=1,y=3] Point[x=2,y=1] Point[x=2,y=2] Point[x=3,y=1] 
--------------------------------------;

transform

指定的范围内应用于给定的操作,并将结果存储在指定的另一个范围内。要使用std::transform函数需要包含<algorithm>头文件。
template<class InputIt1, class InputIt2, class OutputIt, class BinaryOperation>
OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOperation binary_op)
{
    while (first1 != last1) {
        *d_first++ = binary_op(*first1++, *first2++);
    }
    return d_first;
}

例程 - 集合1和集合2中元素相加,放入集合3中

void printV(vector<int>& v){
    int size = v.size();
    printf(":--------------------------------------\n");
    printf("size=%d\n", size);
    auto it = v.begin();
    for(; it != v.end();it++){
        cout << *it << " ";
    }
    printf("\n--------------------------------------;\n");
}

int add(int a, int b) {
    return a + b;
}

int main() {
    vector<int> v1 {1, 2, 3};
    vector<int> v2 {2, 3, 4};
    vector<int> v3(3);

    transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), add);

    printV(v3);
}

//输出
:--------------------------------------
size=3
3 5 7 
--------------------------------------;
*/

string类

#include <cstdio>
#include <iostream>
using namespace std;

int main(int argc, char const *argv[]) {
    string s1 = "abc";
    string s2 = "def";

    s1 += s2;
    s1.append(5, 'a');   //在s1末尾添加5个a
    // puts(s1.c_str());
    // printf("%d\n", s1.length());

    //比较与查找
    s1 = "hello";
    s2 = "hello";
    cout << s1.compare(s2) << endl; // 相等返回0
    cout << (s1 == "hello") << endl;
    cout << s1.compare(2, 5, "hello") << endl;
    cout << s1.find('o', 2) << endl; // 从第二个位置开始找,找到返回下标,找不到返回-1
    cout << s1.find("llo") << endl;
    s2 = s1.substr(2, 3); // 取子串,从下标2取3个字符
    puts(s2.c_str());
    
    //插入与删除
    string s="hello world";
    s.erase(4, 3); // 从下标4开始删除四个字符
    // puts(s.c_str());
    s.insert(s.begin() + 5, ' ');
    s.insert(s.begin() + 6, 'w');
    s.insert(s.begin() + 7, 'o');

    s.insert(5, 3, ' ');
    puts(s.c_str());
    
    
    //替换
    string::iterator it = s.begin();

    s.replace(3, 3, "aaa"); // 从下标3开始,将后面三个字符变成abc [3,5]
    puts(s.c_str());
    
    s.replace(it + 3, it + 6, "bbb");
    puts(s.c_str());
    
    s.replace(3, 3, 3, 'T'); // 从下标3开始,将后面3个字符换成3个T
    puts(s.c_str());
/*
helaaaworld
helbbbworld
helTTTworld
*/
    
    
    
    //拷贝
    char p[20] = "abc";
    s.copy(p, 5, 0); // 从s的0下标开始,拷贝5个字符到p中

    return 0;
}


//字符串分割
#include <cstdio>
#include <iostream>
#include <sstream>
using namespace std;

int main(int argc, char const *argv[]) {
    string s = "hello world";
    istringstream iss(s);
    string token;
    while (getline(iss, token, ' ')) {
        cout << token << endl;
    }
    return 0;
}
/*
hello
world
*/

迭代器

/*
iterator


*/
#include <cstdio>
#include <iostream>
using namespace std;

int main(int argc, char const *argv[]) {
    string s = "hello world";

    string::iterator it_s = s.begin();
    string::iterator it_e = s.end();

    // cout << *it_s << endl; // 迭代器为指针类型,输出值需要加*
    // cout << *(it_e - 1) << endl;
    for (; it_s != s.end(); it_s++) {
        cout << *it_s << endl;
    }
    putchar(10);

    it_s = s.begin();
    while (it_s != s.end()) {
        cout << *it_s;
        it_s++;
    }
    cout << endl;

    it_s = s.begin();
    while (it_s < it_e) {
        cout << *it_s << endl;
        it_s++;
    }

    for (auto it = s.begin(); it < s.end(); it++) {
        printf("%c", *it);
    }
    putchar(10);

    for (auto it : s) {
        printf("%c", it);
    }
    putchar(10);

    return 0;
}

重点

/*
使用基类指针指向派生类成员的方式创建对象时,指针只能访问来自基类的成员,或者重写后的来自基类的成员,不能访问子类中新增的成员
除了指针能够实现多态,引用也可以
*/
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

牛奶奥利奥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值