一、继承和派生
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性.
派生类继承格式
class b
{
};
Class a : pulibc b
{
};
继承方式:
public : 公有继承
private : 私有继承
protected : 保护继承
从继承源上分:
单继承:指每个派生类只直接继承了一个基类的特征
多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基 类的特征
1. 派生类继承方式
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
派生类的访问权限规则如下:
子类会继承父类中所有的内容,包括了私有成员,但私有成员在子类中访问不到,编译器给隐藏了。
(1)公有继承
父类中的protected 在子类中是protected
父类中的public 在子类中是public
(2)保护继承
父类中的protected 在子类中是 protected
父类中的public 在子类中是 protected
(3)私有继承
父类中的protected 在子类中是private
父类中的public 在子类中是private
示例代码:
class father
{
public:
int a;
private:
int b;
protected:
int c;
};
class public_son:public father
{
int d;
};
class private_son:private father
{};
class protected_son:protected father
{};
void test()
{
public_son pubson;
private_son prison;
protected_son proson;
cout << "sizeof(son) = " << sizeof(public_son) << endl; //16
cout << "sizeof(prison) = " << sizeof(private_son) << endl; //12
pubson.a = 33;
//pubson.b = 1;
//prison.a = 10;
//proson.a = 9;
}
2. 继承中的构造和析构
子类对象在创建时会首先调用父类的构造函数,然后调用自身构造,析构顺序与构造顺序相反。
如果父类中没有默认的无参构造,那么子类中的构造函数必须进行传参,使用初始化列表的方式来完成父类的构造。
子类会继承父类的成员属性,成员函数。但是子类不会继承父类构造函数和析构函数。只有父类自己知道如果构造和析构自己的属性,而子类不知道。
示例代码:
class father
{
public:
father(int ma, int mb):a(ma), b(mb) {}
int a;
int b;
};
class son:public father
{
public:
//父类构造没有无参,那么子类的构造必须给父类传参
son(int ma, int mb, int mc):d(ma),father(mb, mc){}
int d;
};
void test()
{
son s1(1, 2 ,3);
cout << "a = " << s1.a << " b = " << s1.b << endl;
}
3. 继承中同名成员的处理方式
(1)如果没有相同的成员,直接调用,调用唯一的(子类或父类)。
(2)如果有同名的成员:
成员属性:直接调用,会调用子类的。如果想要调用父类的,必须指明作用域。
成员函数:
如果子类与父类的成员函数名称相同,子类会把父类的所有的同名版本都隐藏掉,想调用父类的方法,必须加作用域。 s1.父类名::
示例代码:
class BASE
{
public:
BASE(int a):ma(a) { }
void func() {
cout << "father void func" << endl;
}
void func(int a) {
cout << "father int func , a = " << a << endl;
}
int ma;
int mb;
};
class SON:public BASE
{
public:
SON(int a, int b):ma(a), BASE(b) {}
void func(int a) {
cout << "son int func , a = " << a << endl;
}
int ma;
};
void test()
{
SON s1(5, 8);
//成员属性重名,直接调用,调用子类的
cout << "s1.ma = " << s1.ma << endl;
cout << "s1.mb = " << s1.mb << endl; //不重名的,谁有调用谁的
//调用父类的,加作用域
cout << "s1.father.ma = " << s1.BASE::ma << endl;
//成员函数重名,直接调用,调用子类的
s1.func(10);
//错误 重名函数,子类会隐藏掉父类同名的所有版本,调用父类的,必须指明作用域
//s1.func();
//调用父类中的重名函数,必须指明作用域
s1.BASE::func();
s1.BASE::func(88);
}
例2:
class base
{
public:
base():a(0) {}
int a;
};
class son:public base
{
public:
son():a(10){}
void printparmas() {
cout << "son a = " << a << endl;
cout << "base a = " << base::a << endl;
}
void setparams(int ma, int mb) {
this->a = ma;
base::a = mb;
}
int a;
};
void test()
{
son s1;
s1.printparmas();
s1.setparams(66, 88);
s1.printparmas();
}
4. 继承中的静态成员
类似非静态成员函数处理
如果想访问父类中的成员,加作用域即可
示例代码:
class Base
{
public:
static void func() {
cout << "base fun()" << endl;
}
static void func(int a) {
cout << "base fun(int)" << endl;
}
static void funct() {
cout << "base funct" << endl;
}
static int m_A, m_B;
};
int Base::m_A = 10;
int Base::m_B = 31;
class Son :public Base
{
public:
static void func() {
cout << "son fun()" << endl;
}
static int m_A;
};
int Son::m_A = 20;
//静态成员属性 子类可以继承下来
void test()
{
//重名的直接访问访问子类的,访问父类的需要加作用域
cout << Son::m_A << endl;
cout << Son::Base::m_A << endl; //或者Base::m_A
//非重名的,通过子类都可访问
cout << Son::m_B << endl;
cout << Base::m_B << endl;
Son::func();
Son::Base::func(10); //或者Base::func(10);
Son::funct();
}
5. 多继承
Class son:public a, public b
{};
Son s1;
S1.a::func(); s1.b::func();
多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?
解决方法就是通过作用域来进行区分,显示指定调用那个基类的版本。
示例代码:
class Base1
{
public:
Base1():m_A(10) {}
void func() {
cout << "base1 func" << endl;
}
int m_A;
};
class Base2
{
public:
Base2():m_A(20) {}
void func() {
cout << "base2 func" << endl;
}
void func1() {
cout << "base2 func1" << endl;
}
int m_A;
int m_B;
};
//多继承
class Son :public Base1, public Base2 {};
//多继承中很容易引发二义性
void test()
{
cout << sizeof(Son) << endl; //12
Son s1;
//重名的要指定作用域
//s1.m_A; //二义性,不明确
//s1.func(); //二义性,不明确
s1.Base1::func();
s1.Base2::func();
cout << s1.Base1::m_A << endl;
cout << s1.Base2::m_A << endl;
//不重名的,通过派生类直接访问
s1.m_B = 33;
s1.func1();
}
6. 菱形继承和虚继承
两个派生类继承同一个基类, 而这两个派生类又都是一个子派生类的父类,这种继承被称为菱形继承,或者钻石型继承。
1.羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,草泥马继承了羊和驼的成员。当草泥马调用函数或者数据时,就会产生二义性。
2.草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase
{
public:
BigBase():mParam(0) {}
void func() {
cout << "BigBase::func" << endl;
}
public:
int mParam;
};
class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};
void test()
{
Derived derived;
//重复继承,继承了两份BigBase的数据
cout << "size = " << sizeof(derived) << endl; //8
//derived.func(); //二义性,指向不明确
//cout << derived.mParam << endl; //二义性,指向不明确
cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;
}
上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?
对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。在继承方式前面加virtual关键字,这个类就变成了虚基类,虚基类维护了一个虚基类表(虚基类指针)vbtable,这个表记录了类中其他成员的偏移量。虚基类对象中有一个虚基类表vbtable,占一个指针大小长度(32位4字节,64位8字节)。
6.1 采用虚基类方式解决菱形继承问题:
class BigBase{
public:
BigBase():mParam (0) { }
void func() {
cout << "BigBase::func" << endl;
}
public:
int mParam;
};
class Base1 : virtual public BigBase{}; //Base1 虚基类 内部有虚基类表
class Base2 : virtual public BigBase{}; //Base2 虚基类 内部有虚基类表
class Derived : public Base1, public Base2{};
//Derived内部继承了两个虚基类表,但从BigBase继承来的数据只有一份
void test()
{
Derived derived;
cout << "sizeof(Base1) is " << sizeof(Base1) << endl; //8 虚基类表base1 mParam
cout << "sizeof(Base2) is " << sizeof(Base2) << endl; //8
//12 虚基类表base1; 虚基类表base2; int mParam
cout << "sizeof(Derived) is " << sizeof(Derived) << endl;
derived.func();
cout << derived.mParam << endl;
derived.Base1::func();
cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
}
6.2 C++中如果父类的构造需要参数,子类的构造中必须采用初始化列表给父类传参。但虚基类的子类,除了给虚基类传参,还需要给虚基类的父类传参。
虚基类的子类构造时,调用虚基类的构造,但虚基类的构造执行时,不会调用虚基类的父类的构造。只有虚基类的子类会调用虚基类父类的构造,并且传参。
所以说虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。
例1
class BigBase
{
public:
BigBase(int x, int):mParam(x) {
cout << "big base gou zao diao yong, x = " << x << endl;
}
int mParam;
};
class Base1 : virtual public BigBase
{
public:
Base1(int x, int):BigBase(x, 0) { //不调用BigBase构造
cout << "base1 gou zao diao yong" << endl;
}
};
class Base2 : virtual public BigBase
{
public:
Base2(int x, int):BigBase(x, 0) { //不调用BigBase构造
cout << "base2 gou zao diao yong" << endl;
}
};
class Derived : public Base1, public Base2
{
public:
Derived() :Base1(3, 0), Base2(5, 0), BigBase(30, 0){ //调用BigBase构造
cout << "Derived gou zao diao yong" << endl;
}
};
例2:
class BigBase
{
public:
BigBase(int x):mParam(x) {
cout << "big base gou zao diao yong, x = " << x << endl;
}
void func() {
cout << "BigBase::func" << endl;
}
int mParam;
};
class Base1 : virtual public BigBase
{
public:
Base1():BigBase(10) { //不调用BigBase构造
cout << "base1 gou zao diao yong" << endl;
}
};
class Base2 : virtual public BigBase
{
public:
Base2() :BigBase(20){ //不调用BigBase构造
cout << "base2 gou zao diao yong" << endl;
}
};
class Derived : public Base1, public Base2
{
public:
//虚基类的初始化由最后的子类完成
Derived() :BigBase(30){ //调用BigBase构造
cout << "Derived gou zao diao yong" << endl;
}
};
void test()
{
Derived derived;
//打印
//big base gou zao diao yong, x = 30
//base1 gou zao diao yong
//base2 gou zao diao yong
//Derived gou zao diao yong
derived.func();
}
二. 多态-虚函数
1. 继承中父类与子类的类型转换
发生继承关系时,父类与子类可以强制转换。子类强转为父类,子类的可访问范围大于父类,强转为父类后,父类的可访问范围比原来小,是安全的,这称为向上类型转换。父类强转为子类,可寻址范围大于原先父类的合法访问范围,是不安全的,称为向下类型转化。
示例代码:
class Ainimal
{
public:
int a;
};
class Cat:public Ainimal
{
public:
int b;
};
void test()
{
//向上类型转换
Ainimal *a0 = (Ainimal*)new Cat;
a0->b = 13; //a0的访问范围小于c0,安全
//向下类型转换
Cat *c1 = (Cat*) new Ainimal; //c1的访问范围大于a2,可能会越界,非法访问,不安全
c1->b = 23;
}
子类可以隐式类型转换成父类,父类必须显式强制类型转成子类。
class fa
{
public:
fa(int arg):a(arg) {}
int a;
};
class son: public fa
{
public:
son(int arg1, int arg2):fa(arg1),b(arg2) {}
int b;
};
void test()
{
/***************** 子类指针转父类指针 ***********/
son *s1 = new son(12, 45);
fa *f1 = s1;
/************* 子类对象转父类对象/引用 ********/
son s1(12, 45);
fa f1 = s1; //调用拷贝构造
fa &f2 = s1;
cout << s1.a << endl; //12
f1.a = 56;
cout << f1.a << endl; //56
cout << s1.a << endl; //12
f2.a = 33;
cout << s1.a << endl; //33
/***************** 父类对象强制转换子类引用 ***********/
fa f1(12);
//son &s1 = f1; // error: invalid initialization of reference of type ‘son&’ from expression of type ‘fa’
//son s2 = f1; // error: conversion from ‘fa’ to non-scalar type ‘son’ requested
son &s2 = (son&)f1;
//son s3 = (son)f1; //error: no matching function for call to ‘son::son(fa&)’
/**************** 父类指针对象强制转换子类指针 *******/
fa *f1 = new fa(12);
//son *s1 = f1; //error: invalid conversion from ‘fa*’ to ‘son*’ [-fpermissive]
son *s2 = (son*)f1;
}
如果发生了多态,向上向下类型转换总是安全的。但父类转子类还是需要强制类型转换
Ainimal *a1 = new Cat; //向上类型转换
Cat * c1 = (Cat*)a1; //向下类型转换。不会访问越界,安全
2. 虚函数
类成员函数声明前加virtual关键字,声明为虚函数。
类成员中有虚函数时,会在类内生成虚函数表vftable,同时维护一个虚函数表指针vfptr,指向这个虚函数表。如果一个类中有虚函数,那么这个类的大小最小为4(64位下为8),虚函数表指针的大小。
普通虚函数在类中声明,可以实例化对象。
class fa
{
public:
virtual void show_info() { cout << "aa = " << a << endl; };
int a;
};
class son
{
public:
void show_info() { cout << "a = " << a << endl; };
int a;
};
int main() {
fa f1; f1.a = 20;
f1.show_info(); //普通虚函数可以实例化对象
cout << sizeof(fa) << endl; //16
cout << sizeof(son) << endl; //4
}
3. 多态的基本概念:静态多态和动态多态
(1) c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
(2) 静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
(3) 子类继承父类的虚函数,父类指针或引用指向子类对象,发生多态。
发生多态时,子类可以重写或不重写父类虚函数,如果重写,可以省略virtual关键字,写不写都可以。
重写的条件:父类中有虚函数,发生了继承,子类重写的虚函数返回值,函数名字,函数参数,参数顺序,必须和父类完全一致
(4) 多态本质:父类中有虚函数,子类继承父类,父类指针或引用指向子类对象
Void do_speak(Animal &a1) { a1.eat(); }
Cat c1;
do_speak(c1); // Animal &a1 = c1; 父类引用指向子类对象
Ainimal *a2 = new Cat; //父类指针指向子类对象
例1: 静态多态
class Ainimal{
public:
void speak() { cout << "dong wu zai shuo hua" << endl; }
};
void do_speak(Ainimal &ANI)
{ ANI.speak(); }
void test()
{
Ainimal a1; do_speak(a1); //dong wu zai shuo hua
}
调用doSpeak ,speak函数的地址早就绑定好了,早绑定,静态联编,编译阶段就确定好了地址
如果想调用猫的speak,不能提前绑定好函数的地址了,所以需要在运行时候再去确定函数地址,动态联编。
例2:动态多态
动态联编,将speak方法改为虚函数,父类的引用或者指针指向子类对象 Animal & animal = cat
如果发生了继承的关系,编译器允许进行类型转换
class Ainimal{
public: //父类中声明为虚函数,实现了多态,运行时编译
virtual void speak() { cout << "dong wu zai shuo hua" << endl; }
};
class CAT:public Ainimal
{
public: //子类中可重写父类的虚函数 重写virtual可省略
void speak() { cout << "xiao mao zai shuo hua" << endl; }
};
void do_speak(Ainimal &ANI)
{
ANI.speak();
}
void test()
{
Ainimal a1; //普通虚函数,父类可以实例化对象
do_speak(a1); //dong wu zai shuo hua
CAT c1;
do_speak(c1); //xiao mao zai shuo hua 父类引用指向子类对象
Ainimal *a2 = new CAT; //xiao mao zai shuo hua 父类指针指向子类对象
a2->speak();
}
4. 虚函数原理-vfptr虚函数(表)指针
类中有虚函数时会生成一个虚函数表,维护了一个虚函数表指针,指向这个类中的虚函数表。
发生继承时,子类会继承父类的虚函数表和虚函数表指针,构造函数中会将虚函数表指针指向自己的虚函数表。如果子类没有重写父类的虚函数,那么虚函数表和父类是一样的。Animal *a1 = new cat; cat.speak(); 打印动物在说话。
如果子类重写了父类的虚函数,那么子类的虚函数表的函数就替换为子类函数,发生多态时,调用自己本身的函数。Animal *a1 = new cat; a1->speak(); 打印小猫在说话。
5. 纯虚函数(抽象类) 多态案例-计算器
(1) 普通虚函数:可以实例化对象,可以没有子类。
class Ainimal{
public:
virtual void speak() { cout << "dong wu zai shuo hua" << endl; }
};
(2) 纯虚函数-抽象类
class Ainimal{
public:
virtual void speak()=0;
};
虚函数声明后面加’=0’,没有函数体,声明为纯虚函数,这个类叫做抽象类。
纯虚函数(抽象类)无法实例化对象 ,但可以通过多态强制转换
如果发生了继承,必须在子类中实例化这个纯虚函数。
Ainimal a1; //错误
Ainimal *a1 = new Ainimal; //错误。
Ainimal *a1 = new cat; //正确。
(3) 真正的开发中,有个开发原则开闭原则:对扩展开放,对修改关闭。利用多态实现,利于后期扩展,结构性非常好,可读性高。
利用多态实现计算器案例
class jisuanqi
{
public:
//虚函数
//virtual int getResult() { return 0; }
virtual int getResult() = 0;
void set_value(int m_a, int m_b) { this->a = m_a; this->b = m_b; }
int a, b;
/*
纯虚函数 父类中有了纯虚函数,通常又称为抽象类,子类继承父类,就必须要实现纯虚函数.
父类中有了纯虚函数,父类就无法通过类名实例化对象
jisuanqi j1;
jisuanqi*j1 = new jisuanqi
//必须通过多态
jisuanqi *j1 = new jiafa
*/
};
class jiafa:public jisuanqi
{
public:
int getResult() { return a + b; }
};
class jianfa:public jisuanqi
{
public:
int getResult() { return a - b; }
};
void test()
{
jisuanqi *j1 = new jiafa;
j1->set_value(3, 5);
cout << "jia fa = " << j1->getResult() << endl;
delete j1;
j1 = new jianfa;
j1->set_value(3, 5);
cout << "jian fa = " << j1->getResult() << endl;
}
6. 虚析构 纯虚析构
如果父类中有虚函数,被子类继承后,如果发生了多态(必须得是发生了多态, 父类指针指向子类对象),那么父类对象释放时,不会调用子类的析构函数,可能造成子类对象释放不干净的问题。
虚析构就是为了解决子类析构不被调用的问题。
class Ainimal{
public:
virtual void speak() { cout << "dong wu zai shuo hua" << endl; }
Ainimal() { cout << "Ainimal gou zao shan shu" << endl; }
~Ainimal() { cout << "Ainimal xi gou han shu" << endl;}
};
class Cat:public Ainimal
{
public:
Cat(const char* name)
{
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
cout << "Cat gou zao han shu" << endl;
}
~Cat()
{
if (name)
delete[] this->name;
this->name = NULL;
cout << "Cat xi gou han shu" << endl;
}
private:
char *name;
};
void test()
{
//发生多态时,父类普通析构函数调用,子类析构函数不会调用
Ainimal *a1 = new Cat("hu niu");
delete a1;
//Ainimal gou zao shan shu Cat gou zao han shu Ainimal xi gou han shu
//不发生多态时,父类子类析构函数都会调用
Cat c1("tom");
//Ainimal gou zao shan shu Cat gou zao han shu Cat xi gou han shu Ainimal xi gou han shu
}
//父类中析构函数改为虚析构后,发生多态时,子类析构被调用
class Ainimal
{
public:
virtual void speak() { cout << "dong wu zai shuo hua" << endl; }
Ainimal() { cout << "Ainimal gou zao shan shu" << endl; }
//虚析构
virtual ~Ainimal() { cout << "Ainimal xi gou han shu" << endl;}
};
void test()
{
Ainimal *a1 = new Cat("hu niu");
delete a1; //Ainimal gou zao shan shu Cat gou zao han shu Cat xi gou han shu Ainimal xi gou han shu
}
纯虚析构与虚析构功能一样,都是为了解决多态时子类析构不调用的问题。但声明为纯虚析构后,父类也成了抽象类,无法实例化对象。纯虚析构在类中声明后,必须进行定义,声明后紧接着在类外定义。
class Ainimal{
public:
virtual void speak() { cout << "dong wu zai shuo hua" << endl; }
Ainimal() { cout << "Ainimal gou zao shan shu" << endl; }
//纯虚析构 父类变为抽象类,无法实例化对象
virtual ~Ainimal() = 0;
};
Ainimal::~Ainimal() //纯虚析构类内声明,类外定义
{ cout << "Ainimal chuan xi gou han shu" << endl;}
void test()
{
//Ainimal a1;
//Ainimal *a0 = new Ainimal; //无法实例化对象
Ainimal *a1 = new Cat("hu niu");
delete a1; //Ainimal gou zao shan shu Cat gou zao han shu Cat xi gou han shu Ainimal chun xi gou han shu
}
三、指向类成员的指针
1. 指向成员变量的指针
(1) 指针定义 数据类型 类名 ::*指针名 int A::*p;
(2) 指针赋值,初始化
数据类型 类名 ::*指针名 = & 类名::非静态成员变量名
Int A::*p = &A::age;
P = &A::age;
(3) 解引用
类对象名.*指针名
类对象指针->*指针名
class A
{
public:
A(int param):mParam(param){}
public:
int mParam;
};
void test()
{
A a1(100);
int *p1 = &a1.mParam;
*p1 += 2;
cout << "a1.mParam = " << *p1 << endl; //102
A *a2 = new A(200);
int A::*p2 = &A::mParam;
a2->*p2 += 2;
cout << "a2.mParam = " << a2->*p2 << endl; //202
a1.*p2 += 3;
cout << "a1.mParam = " << a1.*p2 << endl; //105
}
2. 指向成员函数的指针
(1) 指针定义
返回值类型 (类名::*指针名) (参数列表)
(2) 指针赋值、初始化
返回值类型 (类名::*指针名) (参数列表) = &类名::非静态成员函数名
指针名 = &类名::非静态成员函数名
(3) 指针解引用 ()最好带上
(类对象名.*指针名)(参数列表)
(类对象指针名->*指针名)(参数列表)
class A{
public:
int func(int a,int b)
{
return a + b;
}
};
void test()
{
A a1;
A* a2 = new A;
//初?始?化ˉ成é员±函ˉ数簓指?针?
int (A::*pFunc)(int, int) = &A::func;
//指?针?解a引皔用?
cout << "(a1.*pFunc)(10, 20) = " << (a1.*pFunc)(10, 20) << endl;
cout << "(a2->*pFunc)(100, 200) = " << (a2->*pFunc)(100, 200) << endl;
}
3. 指向静态成员的指针
指向静态成员(成员变量、成员函数)的指针的定义和使用与普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。
class A
{
public:
static void show_data()
{
cout << "data = " << data << endl;
}
static int data;
};
int A::data = 100;
void test()
{
int *p = &A::data;
*p += 2;
void (*pFunc)() = &A::show_data;
pFunc();
}
四、模板
1. 函数模板
1.1 函数模板的基本使用
通过函数模板生成的函数叫模板函数。
类型参数化 template
告诉编译器紧跟(这个声明只对下面紧跟的一个函数代码有用)的代码里出现T不要报错,T代表类型
(1) 函数模板的使用一: 自动推导类型,显示指明类型
例一:
template <class T>
void myswap(T &t1, T &t2)
{
T tmp; tmp = t1;
t1 = t2;t2 = tmp;
}
void test()
{
int a = 3, b = 4;
//1.自动推导参数类型
myswap(a, b);
//2.显示指定参数类型
double fa = 3.1, fb = 4.2;
myswap<double>(fa, fb);
}
例二:
template <class T>
void my_test(T t1) {
cout << "sizeof = " << sizeof(T) << endl;
}
template <class T> //无法使用T了,必须再次声明template <class T>
void my_test2(T& t1) {
cout << "sizeof = " << sizeof(T) << endl;
}
void test()
{
my_test(32);
my_test<char>(32);
my_test<long>(32);
//如果传参是传引用,那么只能传变量,不能是常量值
//my_test2(10);
//my_test2<int>(10);
int a;
my_test2(a);
my_test2<int>(a);
}
(2) 函数模板的使用二:如果模板函数中得不出T类型,使用时必须指定T的类型,才可以使用
template <class T>
void coutsize()
{
T tmp;
cout << "sizeof(tmp) is " << sizeof(tmp) << endl;
}
void test()
{
coutsize<int>();
coutsize<char>();
}
1.2. 函数模板与普通函数的区别以及调用规则
(1) 区别,普通函数的传参可以进行隐式类型转换,函数模板不可以进行隐式转换。
void test01(int a, int b) {}
template <class T>
void test02(T &t1, T &t2) {}
template <class T>
void test03(T t1, T t2) {}
void test()
{
int a, b; char c;
test01(a, c);
test02(a, b);
//test02(a, c); //不可以进行隐式转换
//test03(a, c); //不可以进行隐式转换
}
(2) 调用规则
如果普通函数和模板函数出现重载,优先使用普通函数调用,如果普通函数没有实现(只有声明),编译报错。如果想强制调用模板,那么可以使用空参数列表
函数模板可以发生重载
如果函数模板可以产生更好的匹配,那么优先调用函数模板
void test01(int a, int b) {
cout << "pu tong han shu diao yong" << endl;
}
template <class T>
void test01(T &t1, T &t2) {
cout << "mo ban han shu diao yong" << endl;
}
template <class T>
void test01(T &t1, T &t2, char) {
cout << "chong zai mo ban han shu diao yong" << endl;
}
//多参数类型模板函数
template <class T1, class T2>
void test01(T1 t1, T2 t2) {
cout << “duo can shu lei xing moban hanshu” << endl;
}
void test01(char a, char b);
void test() {
int a = 0, b = 0; char c = 0, d = 1;
//1.普通函数优先调用
test01(a, b); //pu tong han shu diao yong
//2.使用空参数列表,强制调用函数模板
test01<>(a, b); //mo ban han shu diao yong
//函数模板重载
test01(a, b, c); //chong zai mo ban han shu diao yong
//3.函数模板能够更好的匹配,普通函数还需进行类型转换(强制类型转换实现匹配)
test01(a, c); //duo can shu lei xing moban hanshu
test01<int, char>(a, c); //duo can shu lei xing moban hanshu
//4.普通函数与模板函数重载,优先调用普通函数,普通函数只有声明没有实现,编译报错
//test01(c, d);
}
1.3. 函数模板的机制、局限性、具体化
(1) 模板并不是万能的,不能通用所有的数据类型
(2) 模板不能直接调用,生成后的模板函数才可以调用
(3) 二次编译,第一次对模板进行编译,第二次对替换T类型后的代码进行二次编译
(4) 如果出现不能解决的类型,可以通过具体化来解决问题。
函数模板的具体化
模板函数的具体化必须是已经有了基础数据类型的模板函数之后,才能进行具体化。
模板函数的具体化必须和前面提供的基础数据模板函数参数形式,函数返回值类型都一样。
具体化格式: template<> 返回值 函数名<参数类型> (参数列表)
class person
{
public:
person(int m_age):age(m_age){}
int age;
};
template<class T>
bool mycompare(T &t1, T &t2) {
if (t1 == t2) return true;
return false;
}
template<> bool mycompare<person>(person &p1, person &p2) {
if (p1.age == p2.age) return true;
return false;
}
void test() {
person p1(18), p2(180);
cout << mycompare(p1, p2) << endl;
}
2. 类模板
2.1 类模板的基本使用
(1) 使用方法
template<class TYPE1, class TYPE2 ...>
Class CLAS
{
TYPE1 data1;
……
}
(2)注意事项
函数模板可以进行自动类型推导,而类模板不可以,必须显示指定类型
CLAS<int, string> cl1;
与函数模板区别,可以有默认类型参数
例1:默认参数类型
template<class T = int>
class cl1
{
public:
void printSize() {
cout << "size is " << sizeof(T) << endl;
}
};
void test() {
//使用默认参数类型,可以不传参数类型,但<>不能少
cl1<> c1;
c1.printSize();
cl1<double> c2;
c2.printSize();
cl1<char> c3;
c3.printSize();
}
例2:
template<class nameType, class ageType = int> //可以有默认参数类型
class person
{
public:
person(nameType m_name, ageType m_age):age(m_age), name(m_name){}
nameType name;
ageType age;
};
void test() {
string name = "xiao hong"; int age = 18;
//person p1(name, age); //错误,无法自动推导,必须指定类型
//person p2("xiao hong", 20);
person<string, int> p3(name, age);
person<string, int> p4("xiao hong", 20);
person<string> p5("xiao liang", 21);
}
类模板中的成员函数一开始不会创建出来,而是在运行时才去创建
class Person1
{
public:
void showPerson1() { cout << "Person1的调用" << endl;}
};
class Person2
{
public:
void showPerson2() { cout << "Person2的调用" << endl;}
};
template<class T>
class myClass
{
public:
T obj;
void func1() { obj.showPerson1(); }
void func2() { obj.showPerson2(); }
};
void test()
{
//类模板中成员函数 一开始不会创建出来,而是在运行时才去创建
myClass<Person1>m;
m.func1();
//m.func2(); //无法调用
}
2.2 类模板做函数参数
(1) 显式指定类模板参数传入类型
(2) 类模板参数类型模板化
(3) 函数参数模板化
template<class nameType, class ageType = int> //可以有默认参数类型
class person
{
public:
person(nameType m_name, ageType m_age):age(m_age), name(m_name){}
void show_person()
{ cout << "name is " << this->name << " age is " << this->age << endl;}
nameType name;
ageType age;
};
//1.显示指定类模板参数传入类型
void test01(person<string, int>&p1) {
p1.show_person();
}
//2.类模板参数类型模板化
template<class T1, class T2>
void test02(person<T1, T2>&p1) {
p1.show_person();
//如何查看类型
cout << typeid(T1).name() << endl;
cout << typeid(T2).name() << endl;
}
//3.函数参数模板化
template<class P>
void test03(P &p1) {
p1.show_person();
cout << typeid(P).name() << endl;
}
void test()
{
person<string, int> p1("xiao hong", 20);
test01(p1);
test02(p1);
test03(p1);
}
2.3 类模板的继承
基类如果是模板类,必须让子类告诉编译器 基类中的T到底是什么类型。如果不告诉,那么无法分配内存,编译不过
(1) 子类是普通类,子类声明中显示指定父类参数类型
template<class ageType>
class person
{
public:
ageType age;
};
class son1:public person<int> {
}
(2) 子类是模板类,可以在变量定义时指定类型
template<class ageType>
class person
{
public:
ageType age;
};
template<class T1, class T2>
class son1:public person<T2>
{
T1 name;
}
void test()
{
son1<string, int> s1;
}
2.4 类模板成员函数的类外实现
使用到类名的地方都是带上类型
template<class ageType>
class person
{
public:
person(ageType m_age);
void showPerson();
ageType age;
};
template<class ageType>
person<ageType>::person(ageType m_age)
{
this->age = m_age;
}
template<class ageType>
void person<ageType>::showPerson()
{
cout << this->age << endl;
}
void test()
{
person<int> p1(10);
p1.showPerson();
}
2.5 类模板的分文件编写
分文件编写
Person.h
#pragma once
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class person
{
public:
person(T1 m_name, T2 m_age);
void showPerson();
T1 name;
T2 age;
};
Person.cpp
#include "person.h"
template<class T1, class T2>
person<T1, T2>::person(T1 m_name, T2 m_age)
{
this->name = m_name;
this->age = m_age;
}
template<class T1, class T2>
void person<T1, T2>::showPerson()
{
cout <<this->name << " de age is " << this->age << endl;
}
Main.cpp
#include <iostream>
#include <string>
using namespace std;
#include "person.h"
int main()
{
person<string, int> p1("xiao hong", 18);
p1.showPerson();
system("pause");
return 0;
}
编译报错,2个无法解析的外部命令,链接错误。
在qt编译器顺利通过编译并执行,但是在Linux和vs编辑器下如果只包含头文件,那么会报错链接错误,需要包含cpp文件,但是如果类模板中有友元类,那么编译失败!
解决方案:
类模板的声明和实现放到一个文件中,我们把这个文件命名为.hpp(这个是个约定的规则,并不是标准,必须这么写).
原因:
类模板需要二次编译,在出现模板的地方编译一次,在调用模板的地方再次编译。
C++编译规则为独立编译
person.hpp
#pragma once
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class person
{
public:
person(T1 mname, T2 mage);
void showPerson();
T1 name;
T2 age;
};
template<class T1, class T2>
person<T1, T2>::person(T1 mname, T2 mage) {
this->name = mname;
this->age = mage;
}
template<class T1, class T2>
void person<T1, T2>::showPerson() {
cout << this->name << " age is " << this->age << endl;
}
2.6 类模板碰到友元函数
(1) 在类模板类内实现全局友元函数
类内声明并且实现friend void showperson(person<T1, T2> &p1)
示例代码:
template<class T1, class T2>
class person
{
friend void showPerson(person<T1, T2> &p1) {
cout << "name is " << p1.name << " age is " << p1.age << endl;
}
public:
person(T1 m_name, T2 m_age):name(m_name),age(m_age){}
private:
T1 name;
T2 age;
};
void test()
{
person<string, int> p1("xiao hong", 18);
showPerson(p1);
}
(2) 全局友元函数,类模板内声明,类外实现
//让编译器看到Person类声明
template<class T1, class T2> class person;
template<class T1, class T2> void showPerson(person<T1, T2> & p); //提前声明
template<class T1, class T2>
class person
{
//普通函数与模板函数重载,优先调用普通函数,普通函数只有声明没有实现,编译报错
//friend void showPerson(person<T1, T2> &p1); //这里是普通函数的声明
//利用空参数列表 告诉编译器 模板函数的声明
friend void showPerson<>(person<T1, T2> &p1);
public:
person(T1 m_name, T2 m_age):name(m_name),age(m_age){}
private:
T1 name;
T2 age;
};
template<class T1, class T2>
void showPerson(person<T1, T2> &p1) //模板函数的定义
{
cout << "name is " << p1.name << " age is " << p1.age << endl;
}
void test()
{
person<string, int> p1("xiao hong", 19);
showPerson(p1);
}
五. c++类型转换
1. 静态转换(static_cast)
(1) 用于基类(父类)和派生类(子类)之间指针或引用的转换。
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
(2) 可以进行基础数据类型转换 ,如把int转换成char,把char转换成int。这种转换的安全性也要开发人员来保证。
(3) 没有父子关系的自定义类型不可以转换
(4) 使用方法 static_cast<目标数据类型>(原始数据)
(5) 使用示例
//基础数据类型转换
void test01()
{
float a = 3.23;
int ia = static_cast<int>(a);
cout << "a = " << ia << endl;
}
//父类与子类之间转换
class Base{};
class Child:public Base{};
class Other{};
void test02()
{
//1.父子类指针互相转换
Base *b0 = new Base;
Child *c0 = new Child;
//向下转换,不安全
Child *c1 = static_cast<Child*>(b0);
//向上转换,安全
Base *b1 = static_cast<Base*>(c0);
}
void test03()
{
//2.父子类引用相互转换
Base b0;
Child c0;
Base &b1 = b0;
Child &c1 = c0;
//向下转换,不安全
Child &c2 = static_cast<Child&>(b1);
//向上转换,安全
Base &b2 = static_cast<Base&>(c1);
}
void test04()
{
//无继承关系无法转换
Base *b0 = new Base;
//Other *O1 = static_cast<Other*>(b0);
}
2. 动态转换(dynamic_cast)
(1) 不可以转换基础数据类型
(2) 父子之间指针或引用转换
父转子 不可以
子转父 可以
发生多态 都可以
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全
(3) 使用示例
class Base{};
class Child:public Base{};
class Other{};
void test()
{
//1. 不能转换基础数据类型
float a = 3.23;
int ia = dynamic_cast<int>(a);
//2. 子转父可以,父转子不行
Base *b0 = new Base;
Child *c0 = new Child;
//向下转换,无法转换
//Child *c1 = dynamic_cast<Child*>(b0);
//向上转换,安全
Base *b1 = dynamic_cast<Base*>(c0);
//3. 发生多态时,父子可以互转
//4. 无继承关系无法转换
}
3. 常量转换(const_cast)
(1) 作用:
修改类型的const属性,去掉const或添加const,只能用于指针或引用变量。
(2) 示例代码
//修改指针的const属性
void test01()
{
const int *p = NULL;
//去除const
int *pp = const_cast<int*>(p);
int *p1 = NULL;
//添加const
const int *pp1 = const_cast<const int*>(p1);
const int a = 10; //不能对非指针或非引用进行转换
//int b = const_cast<int>(a);
}
//修改引用的const属性
void test02() {
int num = 10;
int & refNum = num;
const int& refNum2 = const_cast<const int&>(refNum);
}
4. 重新解释转换(reinterpret_cast)
这是最不安全的一种转换机制,最有可能出问题。
主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针
class A{};
class B{};
void test()
{
int a = 33;
int *p = reinterpret_cast<int *>(a);
A *a0 = new A;
B *b0 = reinterpret_cast<B*>(a0);
}
六. 异常
1. 异常的基本使用
(1) 使用说明
try
{
}
catch( 捕获类型 )
{
}
试图执行 try{}中的内容
在可能出现异常的地方抛出异常throw
catch捕获异常
…代表 所有其他类型
如果不想处理异常,继续向上抛出throw
throw抛出异常后,throw下面的代码不会再执行。
如果没有任何处理异常的地方,那么成员调用terminate函数,中断程序
自定义异常类,可以抛出自定义的对象,捕获自定义的异常
(2) 栈解旋
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋
(3) 使用示例
class myException //自定义异常类
{
public:
void printError() {
cout << "自定义异常" << endl;
}
};
class Person
{
public:
Person() { cout << "Person构造" << endl; }
~Person(){ cout << "Person析构" << endl; }
};
int myDevide(int a ,int b)
{
//如果b== 0 抛出异常
if (b == 0)
{
//throw 1; //抛出int类型异常
throw 3.14; //抛出double类型异常 异常必须处理,如果不处理 就挂掉
//栈解旋 构造和析构顺序相反
Person p1;
Person p2;
//throw myException(); //抛出自定义类型异常 匿名对象
}
return a / b;
}
void test01()
{
int a = 10, b = 0;
try //试一试
{
myDevide(a, b);
}
catch (int) //捕获异常
{
cout << "int类型异常捕获" << endl;
}
catch (double f) {
cout << “捕获double类型异常,异常值:” << f <<endl;
}
catch (myException e) {
e.printError();
}
catch (...) {
cout << "其他类型异常捕获" << endl;
throw 1; //继续往上抛 throw抛出异常后throw下面的代码不会被执行
}
cout << "test01 异常处理完成" << endl;
}
void main()
{
try {
test01();
}
catch (...) {
cout << "main函数异常捕获" << endl;
}
}
如果throw抛出的异常是常量,那么最好强制类型转换成期望的类型。不然catch的结果可能跟期望的不一样。
比如:throw 3.14; 如果catch (float &f) { }那么是捕获不到异常的。catch (double &d){} 能够捕获到。
所以最好强制类型转换 thorw (int)2; thorw (float)3.14;
catch捕获到异常后,这个异常就不存在了。即使后面还有catch语句匹配,也不会再次捕获到异常。
2. 异常接口声明
为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型
例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类型的异常。
如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常
例如:void func()
一个不抛任何类型异常的函数可声明为:void func() throw()
如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexcepted函数会被调用,该函数默认行为调用terminate函数中断程序。
//可抛出所有类型异常
void TestFunction01()
{
throw 10;
}
//只能抛出int char char*类型异常
void TestFunction02() throw(int,char,char*)
{
string exception = "error!";
throw exception;
}
//不能抛出任何类型异常
void TestFunction03() throw()
{
throw 10;
}
3. 异常变量的生命周期
(1) 使用说明
catch捕获类型
如果MyException e,会多开销一份数据 ,调用拷贝构造
如果MyExcepiton *e, 不new提前释放对象,new自己管理delete
推荐MyException &e 容易些而且就一份数据
(2) 示例代码
class MyException
{
public:
MyException() { cout << "MyException的默认构造" << endl; }
MyException(const MyException & e) {
cout << "MyException的拷贝构造" << endl;
}
~MyException() { cout << "MyException的析构调用" << endl; }
void printError() { cout << "error" << endl; }
};
void doWork()
{
throw MyException();
throw &MyException();
throw new MyException();
}
void test01()
{
try
{
doWork();
}
//1.捕获变量,调用两次构造(无参构造,拷贝构造),多开销一份数据
catch (MyException e)
//2.捕获变量引用,只调用一次无参构造,变量在catch中也可以使用,生命周期得到 //了延续,在catch处理之后变量调用析构,释放,相比上一种节省了一个开销
catch (MyException &e)
//3.捕获变量指针 传递指针,只会调用一次构造函数。
//如果是&MyException() 或者MyException e1; throw &e1. throw之前变量就被释放了,catch中//不要再使用这个指针了, 否则算非法引用。并且有些编译器不允许取匿名对象的地址,编译通不过
//如果是new MyException(),catch中会传递new出来的对象的地址,catch中可以使用这个地址,需要在catch中手动释放这个指针
catch (MyException *e)
{
//e->printError(); //指向非法内存空间,不应该这么做
cout << "捕获异常" << endl;
//delete e; //靠自觉 释放对象
}
}
4. 异常的多态使用
//异常基类
class BaseException
{
public:
virtual void printError() {}
};
class NullPointerException:public BaseException
{
public:
virtual void printError(){ cout << "空指针异常" << endl; }
};
class OutofRangeException:public BaseException
{
public:
virtual void printError(){ cout << "越界异常" << endl; }
};
void doWork() {
//throw NullPointerException();
throw OutofRangeException();
}
void test01() {
try
{
doWork();
}
catch (BaseException & e) //父类指针或引用指向子类对象,发生多态
{
e.printError();
}
}
5. 使用系统提供的标准异常
(1) 使用方法
#incldue <stdexcept>
throw out_of_range(”aaa”)
catch(out_of_range & e) cout << e.what();
(2) 使用示例
#include <stdexcept>
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
//年龄做检测
if (age < 0 || age > 200)
{
//抛出越界异常
//throw out_of_range("年龄越界了!");
throw length_error("长度越界");
}
}
string m_Name;
int m_Age;
};
void test01() {
try {
Person p("张三",300);
}
catch (out_of_range & e) {
cout << e.what() << endl;
}
catch (length_error & e) {
cout << e.what() << endl;
}
}
6. 编写自己的异常类
(1)方法
自己的异常类 需要继承于 exception
重写 虚析构 what()
内部维护一错误信息 字符串
构造时候传入 错误信息字符串,what返回这个字符串
string转char * .c_str();
(2) 示例代码
const修饰成员函数-常函数:
const修饰的成员函数,函数体内无法修改普通成员变量,变量前加mutable的可修改(mutalbe int a)
class MyOutOfRangeException:public exception
{
public:
MyOutOfRangeException(string errorInfo)
{ this->m_ErrorInfo = errorInfo; }
virtual ~MyOutOfRangeException() {}
virtual const char * what() const
{
//返回 错误信息
//string转char * .c_str()
return this->m_ErrorInfo.c_str();
}
string m_ErrorInfo;
};
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
//年龄做检测
if (age < 0 || age > 200)
{
throw MyOutOfRangeException( string("我自己的年龄越界异常"));
}
}
string m_Name;
int m_Age;
};
void test01() {
try {
Person p("张三", 300);
}
catch ( MyOutOfRangeException & e ) {
cout << e.what() << endl;
}
}
七、输入输出流
1. 概述
(1) 相关类
ios是抽象基类,ios派生出istream ostream, iostream菱形继承istream和ostream。
Istream派生出ifstream,ostream派生出ofstream,iostream派生出fstream。
(2) 相关头文件
iostream
在iostream中定义的类有ios,istream,ostream,iostream等。
在iostream中还定义了4个流对象: Cin,cout,cerr,clog。
Ostream cout(stdout)
cout(console output)流在内存中对应开辟了一个缓冲区,用来存放流中的数据,当向cout流插人一个endl时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符, 并刷新流(清空缓冲区)。注意如果插人一个换行符”\n“(如cout<<a<<"\n"),则只输出和换行,而不刷新cout 流(但并不是所有编译系统都体现出这一区别)。
在iostream中只对"<<“和”>>“运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出进行重载。如果用户声明了新的类型,并希望用”<<“和”>>"运算符对其进行输入输出,按照重运算符重载来做
cerr流对象
cerr流对象是标准错误流,cerr流已被指定为与显示器关联。cerr的 作用是向标准错误设备(standard error device)输出有关出错信息。cerr与标准输出流cout的作用和用法差不多。但有一点不同:cout流通常是传送到显示器输出,但也可以被重定向输出到磁盘文件,而cerr流中的信息只能在显示器输出。
clog流对象
clog流对象也是标准错误流,它是console log的缩写。它的作用和cerr相同,都是在终端显示器上显示出错信息。区别:cerr是不经过缓冲区,直接向显示器上输出有关信息,而clog中的信息存放在缓冲区中,缓冲区满后或遇endl时向显示器输出。
2. 标准输入流对象cin
int get(); 从缓冲区读取一个字符,能够读取到\n
istream& get (char& c); 从缓冲区读取一个字符,能够读取到\n
istream& get (char* s, streamsize n);
//从缓冲区读n个字符到空间s中,不会把换行符拿走,遗留在缓冲区中
cin.getline() //把换行符读取,并且扔掉,所以读到buf中并没有\n
cin.ignore() //忽略缓冲区前n个字符,默认是1
istream& ignore (streamsize n = 1, int delim = EOF);
cin.peek() //int peek(); 偷窥
istream& putback (char c); 放回
void test()
{
char a, b, ca, cb;
int c;
char dest[24] = {0};
a = cin.get(); //等待输入
//输入asd 缓冲区中 a s d 换行
//第一个拿a 第二个拿s 第三次d 第四次拿换行 第五次等待下次输入
cout << a << endl;
b = cin.get();
cout << b << endl;
c = cin.get();
cout << c << endl; //100
cin.get(); //取走换行符
cin.get(ca); cin.get(cb); //等待输入
cout << ca << "," << cb << endl;
cin.get(dest, 3);
cout << cin.get() << endl; //换行符还在缓冲区,10 \n
cout << dest; //没有换行
}
void test() {
char dest[24] = {0};
cin.getline(dest, 24);
cout << cin.get() << endl; //缓冲区没有\n了,等待输入
cout << dest; //读到的没有换行符,读走丢掉了
}
void test() {
cin.ignore(); //输入asdfg
cout << (char)cin.get() << endl; //s
cin.ignore(2);
cout << (char)cin.get() << endl; //g
}
void test() {
//输入asd 偷看一眼a,然后再放回缓冲区 缓冲区中还是asd
cout << (char)cin.peek() << endl; //a
cout << (char)cin.get() << endl; //a
}
void test() {
char buf[24] = {0};
char c = cin.get(); //输入asd
cin.putback('q');
cin.getline(buf, 24);
cout << buf << endl; //qsd
}
//案例2 让用户输入 1 到 10 的数字 ,如果输入有误 重新输入
void test() {
int num;
cout << "请输入一个1到10的数字:" << endl;
while (true)
{
cin >> num;
if (num > 0 && num <= 10) {
cout << "输入的数字为" << num << endl;
break;
} //cout << "对不起,请重新输入" << endl;
//重置标志位
cin.clear();
cin.sync(); //清空缓冲区
cout << "标志位: " << cin.fail() << endl; //标志位 0 正常的 1 不正常
}
}
3. 标准输出流对象cout
输出字符
cout.flush() //刷新缓冲区 Linux下有效
cout.put() //向缓冲区写字符 ostream& put (char c);
cout.write() //从buffer中写num个字节到当前输出流中。ostream& write (const char* s, //streamsize n);
void test() {
//cout.put('a').put('b').put('c').put('\n');
cout.write("abcde\n", strlen("abcde\n"));
}
格式化输出,使用流对象cout的成员函数方法
void test() {
//通过流成员函数
int number = 99;
cout.width(20);
cout.fill('*');
cout.setf(ios::left); //设置格式 输入内容左对齐
cout.unsetf(ios::dec); //卸载十进制
cout.setf(ios::hex); //安装16进制
cout.setf(ios::showbase); //强制输出整数基数 八进制0 十六进制0x
cout.unsetf(ios::hex);
cout.setf(ios::oct);
cout << number << endl;
}
控制符格式化输出
使用时需要包含头文件#include <iomanip>
void test() {
int number = 99;
cout << setw(20)
<< setfill('~')
<< setiosflags(ios::showbase) //基数
<< setiosflags(ios::left) //左对齐
<< hex // 十六进制
<< number
<< endl;
}
4. 文件读写
和文件有关系的输入输出类主要在fstream.h这个头文件中被定义,在这个头文件中主要被定义了三个类,由这三个类控制对文件的各种输入输出操作,他们分别是ifstream、ofstream、fstream,其中fstream类是由iostream类派生而来,他们之间的继承关系见下图所示:
在fstream头文件中是没有像cout那样预先定义的全局对象,所以我们必须自己定义一个该类的对象。ifstream类,它是从istream类派生的,用来支持从磁盘文件的输入(读文件)。ofstream类,它是从ostream类派生的,用来支持向磁盘文件的输出(写文件)。fstream类,它是从iostream类派生的,用来支持对磁盘文件的输入输出。
(1) 打开文件的方法
文件流的成员函数open
ofstream ofs; //定义文件流的对象
ofs.open(“test.txt”, ios::out);
使用文件流对象的有参构造
ifstream ifs(“./test.txt”, ios::in);
(2) 指定文件打开方式
ios::in 以输入方法(从文件输入)打开文件,读方式。如果文件不存在,打开失败。
ios::cout 以输出方式(向文件输入)打开文件,写方式。默认方式,如果已有此文件,则将其原有内容清除。
如果没有此文件,则建立此文件。
ios::app 以输出方式打开文件,写入的数据添加到原文件尾部
ios::ate 打开一个已有的文件,文件指针指向文件尾部
ios::trunc 打开一个文件,如果文件已经存在,删除其中数据。
如果是读文件,文件不存在,设置此值时,仍然打开失败。
ifstream ifs("./t.t2", ios::in |ios::trunc); //打开失败
如果是写文件,文件不存在,则建立新文件。 //打开成功,创建了t.t2
如已指定了ios::cout方式,而未指定ios::app,ios:;ate, ios:;in,则同时默认此方式。
ios::binary 以二进制方式打开一个文件,如不指定此方式则默认ascii方式
ios::nocreate 打开一个已有的文件,如果文件不存在,则打开失败,不建立新文件。
ios:;noreplace 如果文件不存在则建立新文件,如果文件存在则操作失败
ios:;in | ios::out 以输入输出方式打开文件,文件可读可写
几点说明:
新版本的I/O类库中不提供ios::nocreate和ios::noreplace。
每一个打开的文件都有一个文件指针,该指针的初始位置由I/O方式指定,每次读写都从文件指针的当前位置开始。每读入一个字节,指针就后移一个字节。当文件指针移到最后,就会遇到文件结束EOF(文件结束符也占一个字节,其值为-1),此时流对象的成员函数eof的值为非0值(一般设为1),表示文件结束了。
可以用“位或”运算符“|”对输入输出方式进行组合
ios::app | ios::nocreate //打开一个输出文件,在文件尾接着写数据,若文件不存在,则返回打开失败的信息
ios::out l ios::noreplace //打开一个新文件作为输出文件,如果文件已存在则返回打开失败的信息
ios::in l ios::out I ios::binary //打开一个二进制文件,可读可写
但不能组合互相排斥的方式,如 ios::nocreate l ios::noreplace。
(3) 判断文件是否打开成功,关闭文件
ifs.isopen(); 返回bool值 打开成功true 打开失败false
ifs.close();
(4) C++对ASCII文件的读写操作
如果文件的每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符,这个文件就是ASCII文件(或称字符文件)。程序可以从ASCII文件中读入若干个字符,也可以向它输出一些字符。
1)用流插入运算符“<<”和流提取运算符“>>”输入输出标准类型的数据。“<<”和“ >>”都已在iostream中被重载为能用于ostream和istream类对象的标准类型的输入输出。由于ifstream和 ofstream分别是ostream和istream类的派生类;因此它们从ostream和istream类继承了公用的重载函数,所以在对磁盘文件的操作中,可以通过文件流对象和流插入运算符“<<”及 流提取运算符“>>”实现对磁盘 文件的读写,如同用cin、cout和<<、>>对标准设备进行读写一样。
2)用文件流的put、get、geiline等成员函数进行字符的输入输出,:用C++流成员函数put输出单个字符、C++ get()函数读入一个字符和C++ getline()函数读入一行字符。
void test()
{
char buf[1024];
//ofstream ofs("./test.txt", ios::out);
//if (ofs.is_open())
// cout << "打开成功" << endl;
//else
// cout << "打开失败" << endl;
//ofs.put('a');
//ofs.put('b');
//ofs.put('\n');
//ofs << "hello world ha ha" << endl;
//ofs.close();
ifstream ifs("./test.txt", ios::in);
if (ifs.is_open())
cout << "打开成功" << endl;
else
cout << "打开失败" << endl;
//第一种方式,>> 每次读取读到空白符停止,下次读取跳过空白符
while (ifs >> buf) { //1 ab 2 hello 3 world 4 ha 5 ha
cout << buf << endl;
}
//第二种方式,getline 读走\n并丢掉
//while (!ifs.eof()) {
// ifs.getline(buf, sizeof(buf));
// cout << buf << endl;
//}
//第三种方式,get 单个字符读取
//char c;
//while ((c = ifs.get()) != EOF) {
// cout << (char)c << endl;
//}
ifs.close();
}
(5) C++对二进制文件的读写操作
二进制文件不是以ASCII代码存放数据的,它将内存中数据存储形式不加转换地传送到磁盘文件,因此它又称为内存数据的映像文件。因为文件中的信息不是字符数据,而是字节中的二进制形式的信息,因此它又称为字节文件。
对二进制文件的操作也需要先打开文件,用完后要关闭文件。在打开时要用ios::binary指定为以二进制形式传送和存储。二进制文件除了可以作为输入文件或输出文件外,还可以是既能输入又能输出的文件。这是和ASCII文件不同的地方。
用成员函数read和write读写二进制文件
对二进制文件的读写主要用istream类的成员函数read和write来实现。这两个成员函数的原型为
istream& read(char *buffer,int len);
ostream& write(const char * buffer,int len);
字符指针buffer指向内存中一段存储空间。len是读写的字节数。调用的方式为:
a. write(p1,50);
b. read(p2,30);
上面第一行中的a是输出文件流对象,write函数将字符指针p1所给出的地址开始的50个字节的内容不加转换地写到磁盘文件中。在第二行中,b是输入文件流对象,read 函数从b所关联的磁盘文件中,读入30个字节(或遇EOF结束),存放在字符指针p2所指的一段空间内。
class person
{
public:
person(char *m_name, int m_age)
{
strncpy(this->name, m_name, sizeof(this->name));
this->age = m_age;
}
person(){}
char name[64]; //不能为string
int age;
};
int test()
{
person p1("xiao hong", 18);
person p2("xiao liang", 17);
//写操作
//打开binary
ofstream ofs("./test.txt", ios::out | ios::binary);
if (!ofs.is_open()) {
cout << "打开失败" << endl;
return -1;
}
//写入 write
ofs.write((const char*)&p1, sizeof(p1));
ofs.write((const char*)&p2, sizeof(p2));
//关闭
ofs.close();
//读写都可以操作
//打开 binary
ifstream ifs("./test.txt", ios::out | ios::binary);
if (ifs.is_open() == false) {
cout << "打开失败" << endl;
return -1;
}
//读操作 read
person p3, p4;
ifs.read((char*)&p3, sizeof(person));
ifs.read((char*)&p4, sizeof(p4));
cout << "name: " << p3.name << " age: " << p3.age << endl;
cout << "name: " << p4.name << " age: " << p4.age << endl;
关闭
ifs.close();
return 0;
}