C++ 深拷贝与浅拷贝
- 系统默认提供的拷贝构造只会进行简单的值拷贝, 如果成员属性中有指向堆区空间的数据, 那么简单的浅拷贝会导致重复释放内存的异常。
- 解决上述问题, 需要自己提供拷贝构造函数, 进行深拷贝。
C++ 构造函数调用
//括号调用
person p; //调用的默认构造函数
person p1(1);//调用的有参构造函数
person p2(p);//调用拷贝构造函数
//显示
person p3 = person();//调用的默认构造函数
person p4 = person(1);//调用的有参构造函数
person p5 = person(p4);//调用拷贝构造函数
//其他
person p6 = p5;//实际上等于 person p6 = person(p5)
person p7 = 1;//实际上等于 person p7 = person(1)
C++ 构造函数列表初始化语法
在构造函数后面加 ” : 属性(值, 参数) , 属性(值, 参数) …… “,假设我们有个human类, 并且私有成员变量有a, b, c, 下面演示如何用初始化列表方式来初始化abc。
例子:
human() : a(10) , b(20) , c(30)
{
//.........
}
这样就可以成功初始化a, b, c了, 当然这里的10,20,30可以是参数。
C++ 类对象作为成员时的构造析构顺序
当类对象作为类的成员时, 构造顺序是按定义顺序构造成员类对象, 最后才构造自己, 析构时是先析构自己, 最后倒着来析构成员。总结: 析构顺序与构造顺序相反。
C++ explicit 关键字
作用: 防止构造函数中的隐式类型转换
例子:
class human
{
human(int a){}
};
human h = 1; //其实这句代码隐式的进行了转换 等价于 human h = human(1);
怎么禁止隐式类型转换呢? 函数前面添加 explicit 即可
class human
{
explicit human(int a){}
};
human h = 1; //此时这句代码已经编译不通过了
human h(1); //需要这样调用才行
C++ 类静态成员变量
- 编译阶段分配内存
- 所有对象共享数据
- 通过对象访问, 通过类名访问
- 有权限控制
- 类内进行声明, 类外初始化
C++ 类静态成员函数
- 可以访问静态成员变量, 不可以访问普通成员变量
- 普通成员函数都可以访问
- 静态成员函数也有权限
- 可以通过对象访问, 通过类名访问
C++ 单例模式
- 目的: 保证一个类仅有一个实例,并提供一个访问它的全局访问点,主要解决一个全局使用的类频繁地创建与销毁。
- 将默认构造 和 拷贝构造 私有化, 防止在堆与栈上创建新的对象实例
- 内部维护一个 对象指针, 并且私有化
- 对外提供 getIns() 方法来访问这个对象指针
- 保证类中只能实例化唯一的对象
C++ 类对象模型
- 成员变量和成员函数是分开储存的
- 空类的大小为 1
- 只有非静态成员才属于对象身上
C++ this指针
- this指针永远指向当前对象
- this指针可以解决成员变量名称与方法形参名称冲突
- *this 是对象本体
- 非静态成员函数才有this指针
C++ 类的空指针访问成员函数
- 如果成员函数没有用到this,那么空指针可以直接访问
- 如果成员函数用到this指针,就要注意,可以加if判断this是否为空,为空直接返回避免程序崩溃
C++ 常函数和常对象
- 常函数 void func() const {}
- 常函数 原理是通过修饰this指针,const Type *const this
- 常函数 不能修改this指针指向的值
- 若函数已经定义为常函数但还想修改某个成员变量的值, 就要在成员变量定义前面加上mutable进行修饰
C++ 常对象
- 常对象 在对象前加入 const 修饰 , const 对象 名称
- 常对象 不可以调用普通的成员函数,但可以调用常函数
C++ 友元函数和友元类
全局友元函数:
- 全局函数写到类中申明,并且最前面写关键字friend
将整个类作为友元类:
- friend class 类名
- 友元类的权限是单向的不可传递的
将类成员函数作为友元:
- 函数返回值 friend 类名::函数名( ….. );
C++ 运算符重载
“+” 号运算符重载:
- 如果想让自定义数据类型进行 + 运算,那么就需要重载 + 运算符
- 在成员函数 或者 全局函数里重写一个 + 运算符的函数
- 函数名 operator+() { }
- 运算符重载也可以重载多个版本
“<<” 左移运算符重载:
- cout<< 对自定义数据类型进行输出, 重载函数必须写到全局范围中
- 不要随意滥用符号重载
- 内置数据类型 的运算符是不可以重载的
- 如果在重载函数中要操作类的私有成员, 那么就要把重载函数设置为类的友元函数
例子实现:
ostream& operator<<(ostream& cout, human& h)
{
//............
return cout;
}
“++” 运算符前置后置重载:
- (++myint) 前置++ , 先++ 后返回自身
- (myint++) 后置++ , 先保存原有的值, 再内部++, 最后返回临时的数据
例子实现:
myint & operator++( ) //前置++重载 , 引用返回
{
this->a++;
return *this;
}
后置++运算符
myint operator++( int ) //后置++重载 , 有个int占位, 并且按值返回
{
myint tmp = *this;
this->a++;
return tmp;
}
“=” 赋值运算符重载:
- 系统默认给类提供的赋值运算符是简单的值拷贝
- 如果类中有指向堆区的指针, 就会出现深浅拷贝的问题, 所以需要重载 “=” 号赋值运算符
- 如果想链式编程就返回类引用
例子:
human &operator=(human &h)
{
if (this->name != 0) //这里需要判断本类是否有数据, 有的话必须释放掉再拷贝, 否则会内存泄漏
{
delete this->name;
this->name = 0;
}
this->name = new char[strlen(h.name) + 1];
strcpy(this->name, h.name);
return *this;
}
human h1;
human h2;
human h3;
h1 = h2 = h3; //完美执行通过
“[ ]” 中括号运算符重载:
- 需要注意返回数据的引用, 这样才能作为左值, 假设不返回引用的话就无法 xx[xx]=xx 这样的操作。
例子:
int & myint::operator[](int index)
{
return this->pAddress[index];
}
C++ 简单实现智能指针思路
- 写个智能指针类来托管new出来的person对象, 当智能指针类析构时再把托管的person指针给delete掉从而达到智能释放.
- 为了让智能指针类像普通new出来的类一样使用, 就需要重载 “->” 和 ” * “
C++ 继承
继承引出:
- 网页有很多公共的部分, 导致实现的时候有很多重复的代码, 这样就引出了继承
- 基类(父类) 公共的网页
- 派生类(子类) 实现不同的内容
- 可以解决解决代码复用过多
- 语法: class 子类 :(继承方式) 父类
继承方式:
- 不管是 公有, 保护, 还是私有方式继承, 基类中的私有属性, 都不可以继承
公有继承:
- 父类中的protected 在子类中是 protected
- 父类中的public 在子类中是public
保护继承:
- 父类中的protected 在子类中是protected
- 父类中的public 在子类中是 protected
私有继承:
- 父类中的protected 在子类中是private
- 父类中的public 在子类中是private
继承中的对象模型:
- 子类会继承父类中的所有内容, 包括了私有属性, 只是我们访问不到, 被编译器隐藏了
继承中的构造和析构顺序:
- 子类创建对象时, 先调用父类的构造, 然后调用自身构造
- 析构顺序与构造顺序相反
- 子类是不会继承父类构造函数和析构函数
- 补充内容, 如果父类中没有合适的默认构造, 那么子类可以利用初始化列表的方式, 显示的调用父类的其他构造
继承中的同名函数处理:
- 成员属性, 优先调用当前子类的, 如果想调用父类需要加上作用域
- 成员函数, 优先调用当前子类的, 父类所有版本会被屏蔽隐藏掉, 除非加上作用域
继承中的静态成员处理:
- 类似非静态成员函数的处理
- 如果想访问父类中的成员加作用域即可
多继承的概念以及问题:
- 多继承写法 class A:public base1, public base2
- 函数名重名的话加上作用域调用即可
菱形继承问题以及解决:
先看代码:
class Animal
{
public:
int m_Age;
};
//Sheep虚基类继承
class Sheep : virtual public Animal
{
public:
};
//Tuo虚基类继承
class Tuo : virtual public Animal
{
public:
};
class SheepTuo :public Sheep, public Tuo
{
};
int main()
{
SheepTuo st;
st.Sheep::m_Age = 10;
st.Tuo::m_Age = 20;
cout << st.Sheep::m_Age << endl;//需要作用域访问
cout << st.Tuo::m_Age << endl;//需要加作用域访问
cout << st.m_Age << endl; //加上虚继承后, 操作的是同一份数据, 而且不需要加作用域了
return 0;
}
- sheepTuo 内部结构: vbptr 虚基类指针, 指向一张 虚基类表, 再通过表找到偏移量, 最后找到共有的数据
C++ 多态
多态的成立条件:
- 有继承
- 子类重写父类虚函数函数
- 返回值, 函数名称, 函数参数个数, 参数类型, 必须和父类虚函数完全一致
- 子类中 virtual 关键字可写可不写, 建议写
- 类型兼容, 父类指针, 父类引用, 指向 子类对象
多态分类:
- 静态多态 函数重载
- 动态多态 虚函数 继承关系
静态联编:
- 地址在编译阶段就已经绑定好了
动态联编:
- 运行的时候再绑定地址
什么叫多态:
- 父类的引用或指针指向子类对象
多态原理解析:
- 当父类中有了虚函数后, 内部结构就发生了改变
- 内部多了一个 vfptr, vitrual function pointer 虚函数表指针, 指向vftable 虚函数表
- 父类中结构 vfptr &Animal::speak
- 子类中 进行继承, 会继承父类的 vfptr vftable
- 构造函数中 会将虚函数表指针 指向自己的虚函数表
- 如果发生了重写, 会替换掉虚函数表中原有的speak, 改为 &Cat::speak
C++ 抽象类和纯虚函数
- 纯虚函数的写法 virtual void func() = 0; 写了以后这个类就是抽象类了
- 抽象类 不可以实例化对象
- 如果一个类继承了抽象类, 就必须重写抽象类中的纯虚函数
C++ 虚析构和纯虚构
- 虚析构, 虚析构写法 virtual ~类名() , 可以解决通过父类指针指向子类对象时, 子类无法析构问题
- 纯虚析构, 纯虚析构函数写法 virtual func() = 0; 类内申明类外实现, 如果出现了纯虚析构函数, 这个类也算抽象类, 不可以实例化对象
C++ 类的向上类型转换和向下类型转换
- 基类转派生类(向下类型转换) 是不安全的
- 派生类转基类(向上类型转换) 是安全的
- 如果发生了多态, 不管向上还是向下类型转换总是安全的
C++ 函数模板
函数模板基本使用:
- 函数模板语法 template<class 或 typename T> 告诉编译器紧跟的代码里边出现T不要报错
- myfunc(T a, T b) , 类型也需要传入, 类型的参数化
- myfunc(a, b) 自动类型推导按照a, b的类型来替换 T
- myfunc(a,b) 显示指定T类型
函数模板与普通函数的区别以及调用规则:
- C++编译器优先考虑普通函数
- 可以通过空模板实参列表的语法限定编译器只能通过模板匹配
- 函数模板可以像普通函数那样重载
- 如果函数模板可以产生一个更好的匹配, 那么编译器会选择模板
模板的机制:
- 模板并不是万能的, 不能通用所有的数据类型
- 模板不能直接调用, 生成后的模板函数才可以调用
- 模板会二次编译, 第一次对模板进行编译, 第二次对替换模板中的T类型后的代码进行编译
模板的局限性:
- 模板不能解决所有类型
- 如果具体化的函数模板能够与调用时传入的参数类型最佳匹配, 那么编译器选择具体化的版本
- 具体化函数模板语法: template<> 返回值 函数名 <具体类型>( 参数 )
来看一段代码:
#include<string>
#include<iostream>
#include<math.h>
using namespace std;
class person
{
public:
person(string name, int age)
{
m_name = name;
m_age = age;
}
string m_name;
int m_age;
};
template<class T>
bool myCompare(T &a, T &b)
{
if (a == b)
{
return true;
}
return false;
}
int main()
{
person p1("1", 10);
person p2("1", 10);
cout << myCompare(p1, p2);
return 0;
}
以上代码是无法编译通过的, 因为模板无法对比自定义数据类型, 要想解决此问题就需要写出另一个指定了具体数据类型的函数模板, 就像下面这样
template<class T>
bool myCompare(T &a, T &b)
{
if (a == b)
{
return true;
}
return false;
}
//前面函数模板定义也不能丢掉, 因为后面这部分代码是为前面函数模板指定出来一个有具体化数据类型的函数模板(个人感觉类似函数重写)
//模板具体化自定义数据类型
template<> bool myCompare<person>(person &a, person &b)
{
if (a.m_age == b.m_age && a.m_name == b.m_name)
{
return true;
}
return false;
}
还有一种骚操作:
template<class T>
bool mycompare(T a, T b)
{
if (a.age == b.age)
{
return true;
}
return false;
}
int main()
{
person p(10);
person p1(10);
cout<<mycompare(p, p1);
return 0;
}
需要注意的是这种方法就只能对有age属性的类进行操作了…
C++类模板
类模板的使用:
#include<string>
#include<iostream>
#include<math.h>
using namespace std;
template<class T1, class T2 = int> //可以有默认类型
class person
{
public:
person(T1 name, T2 age)
{
this->m_name = name;
this->m_age = age;
}
string m_name;
int m_age;
};
int main()
{
person<string,int>p1("aa", 20);//类模板无法自动类型推导, 需要显示指定类型
return 0;
}
- 类模板跟函数模板使用方法一致, 只不过template<class ….>后边紧跟的是类
- 类模板可以有默认类型参数, 而函数模板不可以
- 函数模板可以自动类型推导, 而类模板不可以
类模板作为函数参数传递
template<class t1,class t2>
class person
{
public:
person(t1 name,t2 age)
{
this->m_name = name;
this->m_age = age;
}
void show()
{
cout << "姓名: " << m_name << " 年龄:" << m_age << endl;
}
private:
int m_age;
string m_name;
};
- 显示指定类型
void dowork1(person <string, int> &p)
{
p.show();
}
int main()
{
person <string, int> p("大哥", 88);
dowork1(p);
return 0;
}
- 参数模板化
template<class t1, class t2>
void dowork2(person <t1, t2> &p)
{
p.show();
}
int main()
{
person <string, int> p("大哥", 88);
dowork2(p);
return 0;
}
- 整体模板化
template<class t>
void dowork3(t &p)
{
p.show();
}
int main()
{
person <string, int> p("大哥", 88);
dowork3(p);
return 0;
}
当基类为模板类时的继承方法
- 基类如果是模板类, 必须告诉编译器基类中的 T 到底是什么类型, 否则父类不知道用什么类型去初始化数据, 导致编译失败
#include<string>
#include<iostream>
#include<math.h>
using namespace std;
template<class t1>
class base
{
public:
base()
{
cout << typeid(t1).name() << endl;
}
t1 m_A;
};
//指定父类T的类型
class child1 :public base<int>
{
public:
};
//或者再写个类模板, 参数模板化
template<class t1, class t2>
class child2 :public base<t2>
{
public:
child2()
{
cout << typeid(t1).name() << endl;
}
t1 m_B;
};
int main()
{
child2<int,int> c;
child1 xx;
return 0;
}
类模板的成员函数类外实现
template<class t1, class t2>
class person
{
public:
person(); //默认构造函数
void show(); //方法
public:
t1 name;
t2 age;
};
template<class t2, class t3>
person<t2,t3>::person()
{
}
template<class t2, class t3>
void person<t2,t3>::show()
{
}
int main()
{
person<string,int> p;
return 0;
}
C++类模板的分文件编写问题以及解决
一般类的声明与实现都是分文件编写的, 但是类模板不能这样做。
解决方案:
- 假设类模板是按照正常的分文件编写, 那么使用时需要引入实现的cpp文件, 而不是头文件。
- 不进行分文件编写, 申明和实现都写到同一个文件中并且把文件后缀改为.hpp, 使用时引入hpp即可(推荐此方法)
hpp是什么意思? 顾名思义就是cpp实现和h头文件混合在一起, 调用时引入hpp即可, 非常适合用来编写公用的开源库。
类模板碰到友元函数
- 友元函数类内进行实现
template<class t1, class t2>
class person
{
//友元函数类内实现, showPerson已经是全局函数了
friend void showPerson(person<t1, t2> &p)
{
cout << "姓名:" << p.m_name << endl;
cout << "年龄:" << p.m_age << endl;
}
public:
person(t1 name, t2 age);
private:
t1 m_name;
t2 m_age;
};
template<class t1, class t2>
person<t1, t2>::person(t1 name, t2 age)
{
this->m_name = name;
this->m_age = age;
}
int main()
{
person<string, int> p("111", 100);
showPerson(p);
}
- 友元函数类外进行实现
template<class nameType, class ageType> class person;
template<typename t1, typename t2> void showPerson(person<t1, t2> &p);
template<class nameType, class ageType>
class person
{
friend void showPerson<>(person<nameType, ageType> &p);
public:
person(nameType name, ageType age);
private:
string m_name;
int m_age;
};
template<class nameType, class ageType>
person<nameType, ageType>::person(nameType name, ageType age)
{
this->m_age = age;
this->m_name = name;
}
template<typename t1, typename t2>
void showPerson(person<t1, t2> &p)
{
cout << p.m_age << endl;
cout << p.m_name << endl;
}
int main()
{
person<string, int> p("大猫", 100);
showPerson(p);
}
C++ 类型转换
静态类型转换 static_cast:
int a = 10;
double b = static_cast<double>(a); //将a转换为double类型
- 使用方式: static_cast<目标类型>(原始数据)
- 可以进行基础数据类型转换
- 父与子之间的类型转换
- 没有父子关系的自定义数据类型是无法进行转换的
动态类型转换 dynamic_cast:
- 使用方式和静态类型转换static_cast 基本一致, 只不过对父类与子类之间的转换要求更为严谨
- 允许子类向上转换为父类, 不允许父类向下转换为子类, 除非发生了多态, 当然多态不管是向下类型转换还是向上类型转换总是安全的
常量类型转换 const_cast:
const int *p = NULL;
int *newp = const_cast<int*>(p); //常量指针转为非常量指针
int *p = NULL;
const int *newp2 = const_cast<const int *>(p); //非常量指针转为常量指针
- 不能直接对非指针或非引用的变量使用const_cast
- 该运算符用来修改类型的const属性
- 常量指针被转化为非常量指针, 并且仍然指向原来的对象
- 常量引用被转化为非常量引用, 并且仍然指向原来的对象
重新解释类型转换 reinterpret_cast:
int *p = NULL;
int a = reinterpret_cast<int>(p); //将指针转为整数
int *p1 = reinterpret_cast<int *>(a);//将整数转为指针
- 主要用于将一种数据类型转换为另一种数据类型, 它可以将一个指针转为一个整数, 也可以将一个整数转为一个指针
- 这是最不安全的一种转换机制, 最有可能出问题
- 非常鸡肋, 不推荐使用
C++异常捕获基本使用
#include <iostream>
using namespace std;
int func(int a, int b)
{
if (b == 0)
{
throw -1; //抛出异常, 谁调用谁处理, -1是int类型的异常
}
return a / b;
}
int main()
{
try //try翻译是尝试的意思
{
cout << func(10, 0) << endl; //尝试执行
}
catch (int) //当func中使用throw抛出int类型异常时, catch捕捉int类型异常
{
cout << "main捕捉到int类型异常" << endl; //func中抛出的int异常
}
catch (...) //捕捉其他异常
{
cout << "其他异常..." << endl; //抛出了其他异常
}
}
- try 尝试执行 try{} 中的内容
- try下面的catch 捕获异常
- catch(异常类型)
- catch(…) 代表捕获其他类型的异常
- 如果没有任何处理异常的地方, 那么会调用terminate函数中断程序
假设不想处理异常, 而是继续往上抛出, 该怎么做?
#include <iostream>
using namespace std;
int func(int a, int b)
{
if (b == 0)
{
throw -1; //抛出异常, 谁调用谁处理, -1是int类型的异常
}
return a / b;
}
void test()
{
try //try翻译是尝试的意思
{
cout << func(10, 0) << endl; //尝试执行
}
catch (int) //捕捉int类型异常
{
throw; //test中并不想处理func中的异常, 而是继续往上抛出, 让main函数中的异常捕获去处理
//cout << "test捕捉到int类型异常" << endl; //func中抛出的int异常
}
catch (...) //捕捉其他异常
{
cout << "其他异常..." << endl; //抛出了其他异常
}
}
int main()
{
try
{
test();
}
catch (int)
{
cout << "mian捕获int类型异常" << endl;
}
}
抛出自定义异常类:
#include <iostream>
using namespace std;
class myExceprion //自定义异常类
{
public:
void printErro()
{
cout << "自定义异常类" << endl;
}
myExceprion() {};
myExceprion(const myExceprion&e)
{
cout << "copy" << endl;
}
};
void func()
{
throw myExceprion(); //抛出自定义异常类
}
int main()
{
try
{
func();
}
catch (myExceprion e)
{
e.printErro();
}
}
栈解旋
#include <iostream>
using namespace std;
class myExceprion //自定义异常类
{
public:
void printErro()
{
cout << "自定义异常类" << endl;
}
myExceprion() {};
myExceprion(const myExceprion&e)
{
cout << "copy" << endl;
}
};
class person
{
public:
person()
{
cout << "person构造" << endl;
}
~person()
{
cout << "person析构" << endl;
}
};
void func()
{
//栈解旋
//从try开始到throw抛出异常之后, 所有的栈上对象都会被释放掉, 这个过程称为栈解旋
//构造和析构顺序相反
person p1;
person p2;
throw myExceprion(); //抛出自定义异常类
}
int main()
{
try
{
func();
}
catch (myExceprion e)
{
e.printErro();
}
return 0;
}
- 从try开始到throw抛出异常之后, throw前所有的栈上对象都会被释放掉, 这个过程称为栈解旋
- 个人理解, 其实就是离开了作用域对象从而自动释放了? 也就是说throw相当于return出函数?
异常的接口声明
#include<iostream>
using namespace std;
void func() throw(int) //throw(int) 代表只允许抛出int类型异常, 抛出不属于int类型异常时会崩溃
{
throw 1;
}
void func1() throw(int, char, double) //规定只能抛出int,char, double 这几种类型
{
throw 3.14;
}
void func2() throw() //throw() 不允许抛出任何异常, 违反规定程序崩溃
{
throw 1; //崩溃
}
int main()
{
try
{
//func();
func2();
}
catch(int)
{
cout<<"int 类型异常捕获.."<<endl;
}
catch(...)
{
cout<<"其他类型异常捕获.."<<endl;
}
return 0;
}
- 如果只想抛出特定类型的异常, 可以使用异常的接口申明
- 使用方式: ” void func() throw(xx) 或 throw()”
- 异常接口声明指定多种类型 ” void func() throw(xx类型, xx类型, xx类型)”
- 声明后, 程序就只能抛出特定异常了, 要是抛出其他异常程序会立马崩溃
- 需要注意的是, 异常接口申明在vs编译器上是不支持的, 而gcc编译器上可以
异常变量的生命周期
using namespace std;
class human
{
public:
//human(const char *in_erro) :erro(in_erro) {}
//void print_erro(){cout << "erro: "<< erro << endl;}
human()
{
cout << "默认构造" << endl;
}
~human()
{
cout << "默认析构" << endl;
}
human(const human&h)
{
cout << "拷贝构造" << endl;
}
private:
//const char *erro;
};
void func()
{
throw human();
}
int main()
{
try {
func();
}
catch (human e)
{
cout << "捕获异常" << endl;
//e.print_erro();
}
catch (...)
{
cout << "其他错误!" << endl;
}
return 0;
}
执行输出:
默认构造
拷贝构造
捕获异常
默认析构
默认析构
可以看到异常变量对象的析构居然是在捕获异常后才被调用, 说明在异常输出之前对象还没被析构, 那么就可以把捕获异常处改为引用方式, 这样一来就减少了不必要的拷贝并且提高了效率。
catch (human &e)
{
cout << "捕获异常" << endl;
//e.print_erro();
}
更改后的程序输出:
默认构造
捕获异常
默认析构
C++标准输入输出流
标准输入流:
char buf[1024];
int x = 3;
cin.get(); //从缓冲区中读取一个字符
cin.get(buf, 1024);//从缓冲区读取字符串, 但不包括换行
cin.getline(buf, 1024);//从缓冲区读取字符串包括换行, 但是会把换行扔掉
cin.ignore(x); //忽略x个字符, 目前x=3, 那么输入123最后缓冲区只有一个换行
cin.peek();//偷看一个字符然后放回缓冲区
cin.putback('c');//将指定字符放回缓冲区
cin.clear();//重置标志位
cin.sync();//清空缓冲区
cin.fail();//读取当前标志位
标准输出流:
int number = 99;
cout.width(20);//输出的字符串宽度为4,不足的会用空格补足
cout.fill('*');//填充*
cout.setf(ios::left);//设置格式, 输入内容左对齐
cout.unsetf(ios::dec);//卸载十进制格式
cout.setf(ios::hex);//安装十六进制格式
cout.setf(ios::showbase);//强制输出整数基数 0, 0x
cout.unsetf(ios::hex);//卸载十六进制;
cout.setf(ios::dec);//安装十进制
cout << 99<< endl;