c++面向对象的程序设计学习笔记

C ++ 面向对象的程序设计


文章目录

  • **C ++ 面向对象的程序设计**
  • 一、从C到C++
    • 1、引用的概念
    • 2、动态内存分配
      • 用new运算符实现动态内存分配
          • 用法一:分配一个变量
          • 用法二: 分配一个数组
          • 用delete运算符释放动态分配的内存
          • 用“delete”释放动态分配的数组,要加“[ ]”
    • 3、内联函数
    • 4、函数重载
    • 5、函数的缺省参数
  • 二、类与对象
    • 1、结构化程序设计
          • 结构化程序设计的不足:
            • (1)结构化程序设计中, 函数和其所操作的数据结构,没有直观的联系。
            • (2)结构化程序设计没有“封装”和“隐藏”的概念
            • (3)重用
    • 2、类与对象
      • 2.1 面向对象的程序设计方法:
      • 2.2 对象的内存分配:
      • 2.3 对象间的运算:
      • 2.4 使用类的成员变量和成员函数:
      • 2.5 类的成员函数和类的定义分开写
      • 2.6 类成员的可访问范围
      • 2.7成员函数的重载及参数缺省
    • 3、构造函数(constructor)
      • 3.1 构造函数对变量的使用
      • 3.2 构造函数在数组中的使用
      • 3.3 复制构造函数
      • 3.3 类型转换构造函数
    • 4、析构函数(destructors)
    • 5、析构函数和构造函数被调用的时间
  • 三、类与对象进阶
    • 1、this指针
    • 2、静态成员变量和静态成员函数
    • 3、成员对象和封闭类(enclosing)
    • 4、常量对象
    • 5、常量成员函数
    • 6、常引用
    • 7、友元(friends)
      • 7.1 友元函数
      • 7.2 友元类
  • 四、运算符的重载
    • 1、运算符重载的基本概念
      • 1.1 运算符重载的需求
      • 1.2 运算符重载的形式
    • 2、赋值运算符的重载
    • 3、运算符重载为友元函数
    • 4、流插入运算符和流提取运算符的重载
    • 5、类型转换 运算符的重载
    • 6、自增,自减运算符的重载
  • 五、继承
    • 1、继承和派生的概念
      • 1.1 派生类的写法
      • 1.2 派生类对象的内存空间
    • 2、继承关系和复合关系
    • 3、覆盖和保护成员
      • 3.1 覆盖
      • 3.2 类的保护成员(protected)
    • 4、 派生类的构造函数
    • 5、公有继承的赋值兼容规则
  • 六、多态
    • 1、虚函数和多态的基本概念
      • 1.1 虚函数
      • 1.2 多态的表现形式
      • 1.2 多态的实现原理
    • 2、虚析构函数
    • 3、纯虚函数和抽象类
  • 七、输入和输出流
    • 1、输入输出流相关的类
      • 1.1 标准流对象
      • 1.2 判断输入流结束
      • 1.3 istream类的成员函数
      • 1.4 输出重定向
    • 2、流操纵算子
      • 2.1 概念
      • 2.2 控制浮点数精度的流操纵算子
      • 2.3 设置域宽的流操纵算子
    • 3、文件读写
      • 3.1 文件和流
      • 3.2 创建文件
      • 3.3 文件名的绝对路径和相对路径
      • 3.4 文件的读写指针
      • 3.5 显示关闭文件
      • 3.6 字符文件读写
      • 3.7 二进制文件的读写
      • 3.8 文件拷贝程序mycopy
  • 八、函数模板和类模板
    • 1、函数模板
    • 2、类模板
    • 2、类模板

一、从C到C++


1、引用的概念

​ 类型名 & 引用名 = 变量名;

	int n = 1;
	int& r = n;
	r = 2;
	cout << r;  
	cout << n;
	n = 3;
	cout << r;

输出结果:2 2 3;引用 r 之后给 r 改变值的同时 n 的值也会改变;

注意:定义引用时一定要将其初始化成引用某个变量;初始化之后,它就一直引用该变量,不会再引用别的变量。

//引用的简单示例
void swap (int& a,int& b){
    int tmp;
    tmp = a; a = b; b = tmp;
}
int m,n;
swap (m,n); // 交换了m,n的值

//引用作为函数的返回值
int n = 4;
int& setvalue(){return n ;}
int main(){
    setvalue() = 40;
    cout << n;  //输出40,在对setvalue函数赋值的同时,也就是在对n进行赋值;
    return 0;
} 

常引用:不能通过常引用去修改其引用的内容:

int n = 100;
const int& r = n; //此时 r 的数据类型为const int&
r = 200;          //编译报错
n = 300;          //正确

2、动态内存分配

用new运算符实现动态内存分配

用法一:分配一个变量

p = new t;

t 是任意类型名,p 是类型为 t*的指针。

动态分配出一片大小为 sizeof(t) 字节的内存空间,并且将该空间的起始地址赋值给p。例如:

int* pn;
pn = new int;
*pn = 5 //此时就向通过new开辟出来的空间存入了5
用法二: 分配一个数组

p = new t [n];
t :任意类型名
p:类型为 t* 的指针
n:要分配的数组的元素的个数,可以是整型表达式

动态分配出一片大小为 n*siezof(t) 字节的内存空间,并且将该空间的起始地址赋值给p

int* pn;
int i = 5;
pn = new int[i*20];
pn[0] = 20;
pn [100] = 30; //编译没问题,运行时导致数组越界

new 运算符的返回值类型:
new t;
new t[n];
这两个表达式返回值的类型都是 t*

用delete运算符释放动态分配的内存

用“new”动态分配的内存空间一定要用“delete”运算符进行释放

delete 指针;// 该指针必须指向new出来的空间;

int* p = new int;
*p = 5;
delete p;
delete p;  //导致异常,一片空间不能被delete多次
用“delete”释放动态分配的数组,要加“[ ]”

delete [ ] 指针;//该指针必须指向new出来的数组

int* p = new int[20];
p[0] = 1;
delete[]p;

3、内联函数

函数调用是有时间开销的。如果函数本身只有几条语句, 执行非常快, 而且函数被反复执行很多次, 相比之下调用函数所产生的这个开销就会显得比较大。为了减少函数调用的开销, 引入了内联函数机制。编译器处理对内联函数的调用语句时, 是将整个函数的代码插入到调用语句处, 而不会产生调用函数的语句。

在函数定义前面加 “inline” 关键字,即可定义内联函数,使用内联函数的目的是为了提高函数的运行速度。

inline int max(int a,int b){
if (a > b) return a;
return b;
}  //弊端:可执行程序的体积增大

4、函数重载

一个或多个函数,名字相同,然而参数个数或参数类型不相同(注意:此时不是返回值类型不同),这叫做函数的重载。

//以下三个函数是重载关系
int max(double f1, double f2){ }   //(1)
int max(int n1, int n2){ }         //(2) 
int max(int n1,int n2, int n3){ }  //(3)
int main(){
    max(3.1,2.1);  //调用(1)
    max(2,4);      //调用(2)
    max(1,2,3);    //调用(3)
    max(1,2.1);    //error,二义性
}

函数重载使得函数命名变得简单;编译器根据调用语句中的实参个数和类型判断应该调用哪个函数。

5、函数的缺省参数

定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值。

void func(int x1,int x2=2,int x3=3){}
int main (){
func(10);      //等效于func(10,2,3)
func(10,8);    //等效于func(10,8,3)
func(10,,8);   //错误,只能最右边的连续若干个参数缺省
}

函数参数的可缺省的目的在于提高程序的课扩充性;即如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。

二、类与对象


1、结构化程序设计

C语言使用结构化程序设计:程序 = 数据结构 + 算法
程序有全局变量以及众多相互调用的函数组成;算法以函数的形式实现,用于对数据结构进行操作。

结构化程序设计的不足:
(1)结构化程序设计中, 函数和其所操作的数据结构,没有直观的联系。

随着程序规模的增加, 程序逐渐难以理解, 很难一下子看出来:
. 某个数据结构到底有哪些函数可以对它进行操作?
. 某个函数到底是用来操作哪些数据结构的?
. 任何两个函数之间存在怎样的调用关系?

(2)结构化程序设计没有“封装”和“隐藏”的概念

要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动时,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护,扩充。

(3)重用

在编写某个程序时,发现需要的某项功能,在现有的某个程序已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间关系错综复杂,要抽取这部分代码,会变得十分困难。

2、类与对象

面向对象的程序 = 类+类+…+类;设计程序的过程就是设计类的过程。

2.1 面向对象的程序设计方法:

. 将某类客观事物共同特点( 属性) 归纳出来, 形成一个数据结构( 可以用多个变量描述事物的属性)
. 将这类事物所能进行的行为也归纳出来, 形成一个个函数,这些函数可以用来操作数据结构( 这一步叫“ 抽象“)
然后, 通过某种语法形式, 将数据结构和操作该数据结构的函数“ 捆绑” 在一起, 形成一个“ 类,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系, 这就是”封装“。
面向对象的程序设计具有“ 抽象” “ 封装” “ 继承” “封装” “多态” 四个基本特点

以计算矩形周长和面积为例:
将长,宽变量和求面积,求周长三个函数“封装”在一起,就能形成一个“矩形类”。
长,宽变量成为该“矩形类”的“成员变量”,三个函数成为该类的“成员函数”。成员变量和成员函数统称为类的成员。

class crectangle{
    public:
    int w,h;
    int area(){
        return w * h;
    }
    int perimeter(){
        return 2*(w+h);
    }
    void init(int w_,int h_){
        w = w_; h=h_;  //初始化函数,用来设置宽和高
    }
}; //必须有分号
int main(){
    int w,h;
    crectangle r;   //用一个类的名字定义了一个变量r,此时r称之为一个对象
    cin >> w >> h;
    r.init(w ,h);   //调用init成员函数,作用在对象r上面
    cout << r.area() << endl << r.perimeter();
}

通过类,可以定义变量。类定义出来的变量,也称为类的实例,就是我们所说的“对象”。
在C++中,类的名字就是用户自己定义的 类型的名字。可以像使用基本类型那样来使用它,比如上面程序中的crectangle就是一种用户自定义的类型。

2.2 对象的内存分配:

和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和;例如上面程序中crectangle类,sizeof(crectangle) = 8
每个对象各有自己的存储空间,一个对象的某个成员变量被改变了,不会影响到另一个对象。

2.3 对象间的运算:

和结构变量一样,对象之间可以用“=”进行赋值,但是不能用“==”,“!=”,">","<" 等进行比较,除非这些运算符经过了“重载”。

2.4 使用类的成员变量和成员函数:

用法一:对象名 . 成员名

crectangle r1,r2;
r1.w = 5;
r2.init(5,4);

此时,init函数作用在r2上,即init函数执行期间访问的w和h是属于r2这个对象的,执行r2 . init不会影响到r1

用法二:指针—>成员名

crectangle r1,r2;
crectangle* p1 = &r1;
crectangle* p2 = &r2;
p1->w = 5;
p2->init(5,4); //init作用在p2指向的对象上

用法三:引用名 . 成员名

crectangle r2;
crectangle & rr = r2;
rr.w = 5;
rr.init(5,4); //rr的值变了,r2的值也变

2.5 类的成员函数和类的定义分开写

class crectangle{
    public:
       int w,h;
       int area();         //成员函数仅在此声明
       int perimeter();
       void init(int w_,int h_);
};
int crectangle::area(){
    return w * h;
}
int crectangle::perimeter(){
    return 2 * (w + h);
}
void crectangle::init(int w_,int h_){
     w = w_;
     h = h_;
}
//crectangle::说明后面的函数时crectangle类的成员函数,而非普通函数。那么一定要通过对象或对象的指针或对象的引用才能调用。

2.6 类成员的可访问范围

在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:
private:私有成员,只能在成员函数内访问;
public:公有成员,可以在任何地方访问;
protected:保护成员对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,就变成private;

以上三种关键字出现的次数和先后顺序都没有限制,如果某个成员前面没有上述关键字,则缺省地被认为是私有成员。

class name{
    private:
    私有属性和函数
    public:
    公有属性和函数
    protected:
    保护属性和函数
};

(1)在类的成员函数内部,能够访问:
当前对象的全部属性、函数;
同类其他对象的全部属性、函数;

(2)在类的成员函数以外的地方,只能够访问该类对象的公有成员;

(3)设置私有成员的机制,叫**“隐藏”**;目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改 后,只需要更改成员函数即可,否则,所有直接访问成员变量的语句都需要修改。

2.7成员函数的重载及参数缺省

成员函数也可以重载,也可以带缺省参数;使用缺省参数时要注意避免重载时的二义性

class location{
    private:
       int x,y;
    public:
    void init(int x=0,int y=0){;}
    void valuex(int val){x = val;}
    int  valuex(){return x;}
};
int main(){
    location a,b;
    a.init(5);
    a.valuex(5);
    cout << a.valuex();
}

3、构造函数(constructor)

构造函数是成员函数的一种,名字与类名相同,可以有参数,不能有返回值(void也不行)
作用是对对象进行初始化,如给成员变量赋初值;

如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数,默认构造函数无参数,不做任何操作。
如果定义了构造函数,则编译器不生成默认的无参数的构造函数。

对象对象生成时构造函数自动被调用,对象一旦生成,就再也不能在其上执行构造函数。

3.1 构造函数对变量的使用

class complex{
    private:
       double real,imag;
    public:
       void set(double r,double i);
};  //编译器自动生成默认构造函数
complex c1;  //默认构造函数被调用
complex* pc = new complex;  //默认构造函数被调用
class complex{
    private:
       double real,imag;
    public:
       complex(double r,double i = 0); //这是自己编写的一个构造函数
};
complex::complex(double r,double i){
    real = r; imag = i;
}
complex c1;  //error,缺少构造函数的参数
complex* pc = new complex;  //error,没有参数
complex c2(2);  //正确
complex* pc = new complex(3,4);  //正确

一个类可以有多个构造函数,参数个数或者类型不同,即重载的关系

class complex{
    private:
       double real,imag;
    public:
      void set(double r,double i);
      complex (double r,double i);
      complex (double r);
      complex (complex c1,complex c2);  //三个构造函数重载
};
complex::complex(double r,double i){
    real = r; imag = i;
}
complex::complex(double r){
    real = r; imag = 0;
}
complex::complex(complex c1,complex c2);{
    real = c1.real+c2.real;
    imag = c1.imag+c2.imag;
}
complex c1(3),c2(1,0),c3(c1,c2);

3.2 构造函数在数组中的使用

class csample{
    int x;
 public:
    csample(){
    cout << "constructor 1" << endl;
  }
    csample(int n){
        x = n;
        cout << "constructor 2" << endl;
    }
};
int main(){
    csample array1[2];            //无参,第一个函数初始化
    cout << "step1" << endl;
    csample array2[2] = {4,5};    //有参,第二个函数初始化
    cout << "step2" << endl;
    csample array3[2] = {3};      //一个参数,array[0]第一个函数初始化,array[1]第二个函数初始化
    cout << "step3" << endl;
    csample* array4 = new csample[2];  //开辟的空间中无参,第一个函数初始化
    delete []array4;
}
class test{
    public:
      test(int n){}        //(1)
      test(int n,int m){}  //(2)
      test(){}             //(3)
};
test array1[3] = {1,test(1,2)};  //三个元素分别用(1)(2)(3)进行初始化
test array2[3] = {test(2,3),test(1,2),1};  //三个元素分别用(2)(2)(1)进行初始化
test* pArray[3] = {new test(4),new test(1,2)};  //两个元素分别用(1)(2)进行初始化,pArray[2]是未经初始化的指针

3.3 复制构造函数

只有一个参数,即对同类对象的引用。
形如 X::X ( X& )或X::(const X &),二者选一,后者能以常量对象作为参数
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制的功能。

class complex{
    private:
       double real,imag;
};
complex c1;   //调用缺省无参构造函数
complex c2(c1);  //调用缺省的复制构造函数,将c2初始化和c1一样

如果定义的自己的复制构造函数,则默认的复制构造函数不存在。

class complex {
    public:
       double real,imag;
    complex(){}
    complex(const complex & c){
        real = c.real;
        imag = c.imag;
        cout << "copy constryctor";
    }
};
complex c1;
complex c2(c1);

不允许有形如 X::X( X )的构造函数,参数必须是引用,不能是对象

复制构造函数起作用的三种情况:

(1)、当用一个对象去初始化同类的另一个对象时

       complex c2(c1);   //用c1去初始化c2,此时的参数就是c1
       complex c2 = c1;  //初始化语句,非赋值语句

(2)、如果某函数有一个参数是类A的对象,那么该函数被调用时,类A的复制构造函数将被调用

class A{
    public:
    A(){};
    A( A & a ){
        cout << "copy constryctor" << endl;
    }
};
void func(A a1){}  //a1是对象并且作为参数,一定是使用复制构造函数初始化
int main(){
    A a2;
    func(a2);
}

(3)、如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用

class A{
    public:
        int v;
    A(int n){v = n;};
    A(const A & a){
        v = a.v;
        cout << "copy constructor";
    }
};
A func(){
    A b(4);
    return b;   //复制构造函数的参数就是b
}
int main (){
    cout << func().v << endl;
}

注意:对象间的赋值并不导致赋值构造函数被调用

常量引用参数的使用:

void fun (cmyclass obj_ ){
   cout << "fun" << endl;
}

这样的函数,调用时生成形参会引发复制构造函数的调用,开销较大;
所以可以考虑使用 cmyclass& 引用类型作为参数;
如果希望确保实参的值在函数中不应被改变,那么可以加上 const 关键字;

void fun(const cmyclass & obj){
//函数中任何试图改变obj值的语句都将是变成非法
}

3.3 类型转换构造函数

​ 定义转换构造函数的目的是实现类型的自动转换;
​ 只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数;
​ 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)

class complex {
    public:
      double real,imag;
    complex (int i){  //类型转换构造函数
        cout << "intconstructor" << endl;
        real = i; imag = 0;
    }
    complex(double r,double i) {real = r; imag = i;}
};
int main(){
    complex c1(7,8);
    complex c2 = 12;
    c1 = 9;   // 9被自动转换成一个临时complex对象
    cout << c1.real << "," << c1.imag << endl;
}

4、析构函数(destructors)

名字和类名相同,在前面加“~”,没有参数和返回值,一个类最多只能有一个析构函数。
析构函数在对象消亡时即自动被调用,可以定义析构函数来在对象消亡前做善后工作,比如释放分配内存空间等。
如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。

析构函数实例

class string{
    private:
      char* p;
    public:
      string(){
          p = new char[10];
      } 
    ~string ();
};
string::~string(){
   delete [] p;  //释放内存空间
}

对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。

class ctest{
    public:
    ~ctest(){ cout << "destructor" << endl;
    }
};
int main(){
    ctest array[2];
    cout << "end" << endl;
}

析构函数和运算符delete

//delete运算导致析构函数调用
ctest* ptest;
ptest = new ctest;  //构造函数调用
delete ptest;  //析构函数调用
ptest = new ctest[3];  //构造函数调用3次
delete [] ptest;  //析构函数调用3次

若new一个对象数组,那么用delete释放时应该写[ ],否则只delete一个对象(调用一次析构函数)

析构函数在对象作为返回值返回后被调用

class cmyclass{
    public:
    ~cmyclass(){cout << "destructor" << endl;}
};
cmyclass obj;
cmyclass fun (cmyclass obj){  //参数对象消亡也会导致析构函数被调用
    return sobj;  //函数调用返回时生成临时对象返回
}
int main(){  //函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用
    obj = fun(obj);
}

5、析构函数和构造函数被调用的时间

class demo {
    int id;
    public:
      demo(int i){
          id = i;
          cout << "id = " << id << "constructed" << endl;
      }
    ~demo(){
        cout << "id = " << id << "constructed" << endl;
    }
};
demo d1(1);
void func(){
    static demo d2(2);
    demo d3(3);
    cout << "func" << endl;
}
int main(){
    demo d4(4);
    d4 = 6;
    cout << "main" << endl;
    {demo d5(5);}
    func();
    cout << "main ends" << endl;
}

三、类与对象进阶


1、this指针

作用是指向成员函数所作用的对象

非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针。

class complex{
    public:
    double real,imag;
    void print(){cout << real <<","<< imag;}
    complex(double r,double i):real(r),imag(i)
    {  }
    complex addone(){
        this->real++;     //等价于real++
        this->print();    //等价于print
        return this;
    }
};
int main(){
    complex c1(1,1),c2(0,0);
    c2 = c1.addone();
}

2、静态成员变量和静态成员函数

​ 静态成员:在定义前面加了static关键字的成员

​ 普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。

​ 普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。

​ 如何访问静态成员:

//类名::成员名
  tangle::total();
//对象名.成员名
  tangle r;
  r.total();
//指针->成员名
  tangle* p = &r;
  p->total();
//引用.成员名
  tangle & ref = r;
  int n = ref.total;

​ 静态成员变量本质上是全局变量, 哪怕一个对象都不存在,类的静态成员变量也存在。
​ 静态成员函数本质上是全局函数。
​ 设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面, 看上去像一个整体, 易于维护和理解。

在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。

3、成员对象和封闭类(enclosing)

有成员对象的类叫封闭类。

class ctyre{
    private:
       int radius;
       int width;
    public:
       ctyre(int r,int w):radius(r),width(w){  }
};
class cengine{};
class ccar{   //封闭类
    private:
       int price;
       ctyre tyre;        //其他函数的成员变量,该函数的成员对象
       cengine engine;    //成员对象
    public:
       ccar(int p,int tr,int tw);
};
ccar::ccar(int p,int tr,int w):price(p),tyre(tr,w){}; //初始化列表实现成员变量的初始化,实际上是用无参构造函数初始化
int main(){
    ccar car(20000,17,225);
}  //如果ccar不定义构造函数,ccar car 就会编译出错,因为编译器不明白car.tyre该如何初始化。而car.engine没问题用默认构造函数即可

任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。
具体的做法就是:通过封闭类的构造函数的初始化列表。
成员对象的初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行。

封闭类构造函数和析构函数的执行顺序:

. 封闭类对象生成时, 先执行所有对象成员的构造函数, 然后才执行封闭类的构造函数。
. 对象成员的构造函数调用次序和对象成员在类中的说明次序一致与它们在成员初始化列表中出现的次序无关。
. 当封闭类的对象消亡时, 先执行封闭类的析构函数, 然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。

封闭类的复制构造函数:

封闭类的对象,如果是用默认复制构造函数初始化的,那么它里面包含的成员对象,也会用复制构造函数初始化。

class A{
    public:
       A(){cout << "default" << endl;}
       A(A & a){cout << "copy" << endl;}
};
class B{A a;};
int main(){
    B b1,b2(b1);
}

以上函数中,b2.a是用类A的复制构造函数初始化的。而且调用复制构造函数时的参数就是b1.a

4、常量对象

如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加const关键字。

class demo{
    private:
       int value;
    public:
       void setvalue(){}
};
const demo obj;   //常量对象

5、常量成员函数

在类的成员函数说明后面可以加const关键字, 则该成员函数成为常量成员函数
常量成员函数执行期间不应修改其所作用的对象。因此,在常量成员函数中不能修改成员变量的值**(静态成员变量除外),也不能调用同类的非常量成员函数( 静态成员函数除外)**。

class sample{
    public:
       int value;
       void getvalue() const;
       void func(){};
       sample(){}
};
void sample::getvalue() const{
    value = 0;  //error,常量成员函数在修改的过程中不应该修改其所对应的对象的值
    func();     //error,常量成员函数内部不能调用其他的非常量函数
}
int main(){
    const sample i;
    i.value = 100;   //error,常量对象不可被修改
    i.func();        //error,常量对象上面不能执行非常量成员函数
    i.getvalue();    //ok,常量对象上面可以执行常量成员函数
}

注意:两个成员函数,名字和参数表都一样,但是一个是const,一个不是,算重载。

6、常引用

在引用前面可以加const关键字,成为常引用。不能通过常引用,修改其引用的变量。

对象作为函数的参数时,生成该参数需要调用复制构造函数,效率比较低;此时可以用对象的引用作为参数

7、友元(friends)

7.1 友元函数

​ 一个类的友元函数可以访问该类的私有成员。一个类的友元函数不是该类的成员函数

class ccar;   //提前声明ccar类,以便于后面的cdriver类使用
class cdriver{
    public:
       void modifycar(ccar * pcar);
};
class ccar{
    private:
       int price;
    friend int mostecar(ccar cars[],int total);   //声明友元
    friend void cdriver::modifycar(ccar* pcar);
};
void cdriver::modifycar(ccar* pcar){
    pcar->price += 1000;
}
int mostecar(ccar cars[],int total){
    int tmpmax = -1;
    for (int i=0; i tmpmax)
            tmpmax = cars[i].price;
        return tmpmax;
    }
}
int main(){
    return 0;
}

我们可以将一个类的成员函数(包括构造函数、析构函数)说明为另一个类的友元。

class b{
    public:
      void function();  
};
class a{
    friend void b::function();
};

7.2 友元类

如果A是B的友元类,那么A的成员函数可以访问B的私有成员

class ccar{
    private:
      int price;
    friend class cdriver;
};
class cdriver{
    public:
      ccar mycar;
      void modifycar(){
          mycar.price += 1000;
      }
};
int main(){ return 0;}

友元类之间的关系不能传递,不能继承

四、运算符的重载


1、运算符重载的基本概念

1.1 运算符重载的需求

​ C++预定义的运算符,只能用于基本数据类型的运算:整型、实型、字符型、逻辑型

​ 运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同
​ 类型的行为

​ 运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象。

​ 同一运算符,对不同类型的操作数,所发生的行为不同。

1.2 运算符重载的形式

运算符重载运算符重载的实质是函数重载;
可以重载为普通函数, 也可以重载为成员函数;
把含运算符的表达式转换成对运算符函数的调用;
把运算符的操作数转换成运算符函数参数;
运算符被多次重载时, 根据实参的类型决定调用哪个运算符函数;

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

class complex{
    public:
      double real,imag;
      complex(double r = 0.0,double i=0.0):real(r),imag(i){  }
      complex operator - (const complex & c);
};
complex operator + (const complex & a,const complex & b){
    return complex(a.real + b.real,a.imag + b.imag);  //返回一个临时对象
}
complex complex::operator - (const complex & c){
    return complex(real - c.real,imag - c.imag);   //返回一个临时对象
}
int main(){
    complex a(4,4),b(1,1),c;
    c = a + b;   //等价于c = operator + (a,b);
    cout << c.real << "," << c.imag << endl;
    cout << (a-b).real << "," << (a-b).imag << endl;
    //a-b等价于a.operator
}

重载为成员函数时,参数个数为运算符目数减一;
重载为普通函数时,参数个数为运算符目数;

2、赋值运算符的重载

复制运算符 “=” 只能重载为成员函数

class string{
    private:
       char* str;
    public:
       string():str(new char[1]){str [0] = 0;}
       const char* c_str(){return str;};
       string & operator = (const char* s);
       string::~string( ){delete [] str;}
};
string & string::operator = (const char* s)
{    //重载 “=” 以使得 obj = "hello" 能够成立
    delete [] str;
    str = new char[strlen(s)+1];
    strcpy(str, s);
    return* this;
}
int main(){
    string s;
    s = "good";                   //等价于s.operator = ("good");
    cout << s.c_str() << endl;
    //string s2 = "hello";        //error,这句话是初始化语句而不是赋值语句
    s = "luck";                   //等价于s.operator
    cout << s.c_str() << endl;
}

浅拷贝和深拷贝

class String{
    private:
         char* str;
    public:
         String():str(new char[1]) {str[0] = 0;}
         const char* c_str() {return str;};
         String & operator = (const char* s){
             delete[] str;
             str = new char[strlen(s)+1];
             strcpy(str,s);
             return *this;
         };
       ~String( ) {delete[] str;}
};
int main(){
    String s1,s2;
    s1 = "this";
    s2 = "that";
    s1 = s2;
}

如果不等于自己的运算符,那么s1 = s2实际上导致s1.strs2.str指向同一个地方;
如果s1对象消亡,析构函数将释放s1.str指向的空间,则s2消亡时还要释放一次;
另外,如果执行s1 = “other”;会导致s2.str指向的地方被delete
因此要在class string里添加成员函数:

string & operator = (const string & s){
    if (this == & s)
        return * this;
    delete[] str;
    str = new char[strlen(s.str)+1];
    strcpy(str,s.str);
    return *this;
}

对operator = 返回值类型的讨论

对运算符进行重载的时候,好的风格应该是尽量保留运算符原本的特性

为string类编写复制构造函数时,会面临和 “=” 一样的问题,用同样的方法处理

string (string & s){
    str = new char[strlen(s.str)+1];
    strcpy(str,s.str);
}

3、运算符重载为友元函数

​ 一般情况下,将运算符重载为类的成员函数,是较好的选择。
​ 但是有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。

class complex{
    double real,imag;
    public:
       complex(double r,double i):real(r),imag(i){ };
       complex operator+(double r);
};
complex complex::operator+(double r){  //能解释C+5
    return complex(real + r,imag);
}
//经过上述重载后:
complex c;
c = c+5;   //有定义,相当于c = c.operator+(5);
//但是 c = 5 + c; 编译出错
//所以为使得上述表达式能成立,需要将+重载为普通函数
complex operator+ (double r,const complex & c){  //能解释5+c
    return complex(c.real + r,c.imag);
}

但是上述普通函数又不能访问私有成员,所以需要将运算符 + 重载为友元

class complex{
    double real,imag;
    public:
       complex(double r,double i):real(r),imag(i){ };
       complex operator + (double r);
    friend complex operator + (double r,const complex & c);
};

4、流插入运算符和流提取运算符的重载

​ 为什么**cout << 5 << “this”;**能够成立?

cout 是在iostream中定义的,是ostream类的对象
“<<” 能用在cout上是因为,在iostream里面对 “<<” 进行了重载

ostream & ostream::operator << (int n){
    ........//输出n的代码
        return *this;
}
ostream & ostream::operator << (const char* s)
    ........//输出n的代码
        return *this;
}
//此时cout << 5 << "this";
cout.operator << (5).operator <<("this");

5、类型转换 运算符的重载

​ 作用就是把对象转换为该类型

class complex{
    double real,imag;
    public:
      complex(double r=0,double i=0):real(r), imag(i) { };
      operator double(){return real;}   //重载强制类型转换运算符double
};
int main(){
    complex c1(1.2,3.4);
    cout << (double)c << endl;  //输出1.2
    double n = 2 + c;           //等价于 double n = 2+c.operator double()
    cout << n;                  //输出 3.2
}

6、自增,自减运算符的重载

​ 自增运算符 ++,自减运算符 **-- ** 有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,C++规定:

​ 前置运算符作为一元运算符重载

//重载为成员函数:
t & operator++();
t & operator--();
//重载为全局函数:
t1 & operator++(t2);
t1 & operator--(t2);

后置运算符作为二元运算符重载,多写一个没用的参数:

//重载为成员函数:
t & operator++(int);
t & operator--(int);
//重载为全局函数:
t1 & operator++(t2,int);
t1 & operator--(t2,int);
class cdemo{
    private:
       int n;
    public:
       cdemo(int i = 0):n(i){ }
       cdemo & operator++();
       cdemo operator++(int);
       operator int(){return n;}
       friend cdemo & operator--(cdemo & );
       friend cdemo operator--(cdemo & ,int);
};
cdemo & cdemo::operator(){   //前置 ++
    ++ n;
    return * this;
}   //++s即为:s.operator++();
cdemo cdemo::operator++(int k){  //后置 ++
    cdemo tmp(*this);   //记录修改前的对象
    n++;
    return tmp;    //返回修改前的对象
}   //s++ 即为:s.operator++(0);
cdemo & operator--(cdemo & d){   //前置 --
    d.n--;
    return d;
}   //--s 即为:operator--(s);
cdemo operator--(cdemo & d,int){  //后置 --
    cdemo tmp(d);
    d.n--;
    return tmp;
}  //s-- 即为:operator--(s,0);
int main(){
    cdemo d(5);
    cout << (d++) << ",";   //等价于d.operator++(0);
    cout << d << ",";
    cout << (++d) << ",";   //等价于d.operator++();
    cout << d << endl;
    cout << (d--) << ",";   //等价于operator--(d,0);
    cout << d << ",";
    cout << (--d) << ",";   //等价于operator--(d);
    cout << d << endl;
}

运算符重载的注意事项:

1、C++不允许定义新的运算符;

2、重载后运算符的含义应该符合日常习惯;
complex_a + complex_b
word_a > word_b
date_b = date_a + n

3、运算符重载不改变运算符的优先级;

4、以下运算符不能够被重载:“.” 、 “.*” 、 “::” 、 “?:” 、 "sizeof"

5、重载运算符 ( )、[ ]、-> 或者赋值运算符 = 时,运算符重载函数必须声明为类的成员函数

五、继承


1、继承和派生的概念

继承:在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么就可以把A作为一个基类,而把B作为基类的一个派生类(也称子类)。

派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数
​ 派生类一经定义后,可以独立使用,不依赖于基类

派生类拥有基类的全部成员函数和成员变量,不论是private、protected、public;在派生类的各个成员函数中,不能访问基类的private成员

1.1 派生类的写法

class 派生类名:public 基类名{ };

class cstudent {
    private:
       string sname;
       int nage;
    public:
       bool good(){};
       void setname (const string & name)
       {sname = name;}
};
class custudent:public cstudent{
    private:
       int ndepartment;
    public:
       bool good(){};    //覆盖,也就是对基类的修改
       bool baoyan(){};
};
class cgstudent:public cstudent{
    private:
       int ndepartment;
       char szmentorname[20];
    public:
       int countsalary(){};
};

1.2 派生类对象的内存空间

派生类对象的体积, 等于基类对象的体积, 再加上派生类对象自己的成员变量的体积。在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前

class cbase{
   int v1,v2;
};
class cderived:public cbase{
   int v3;
};

在上述代码中,基类cbase的体积为v1+v2,派生类cderived的体积为v1+v2+v3

2、继承关系和复合关系

​ 继承:“是” 关系
​ 基类A,B是基类A的派生类;则逻辑上要求:“一个B对象也 一个A对象”

​ 复合:“有” 关系
​ 类C中 “有” 成员变量k,k是类D的对象,则C和D是复合关系;一般逻辑上要求:“D对象是C对象的固有属性或组成部分”

3、覆盖和保护成员

3.1 覆盖

​ 派生类可以定义一个和基类成员同名的成员, 这叫覆盖。在派生类中访问这类成员时, 缺省的情况是访问派生类中定义的成 员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号: :

class base{
    int j;
 public:
    int i;
    void func();
};
class derived:public base{
    public:
       int i;
       void access();
       void func();
};
void derived::access(){
    j = 5;          //error,j是基类的私有成员
    i = 5;          //缺省情况,引用的是派生类的i
    base::i = 5;    //引用的是基类的i
    func();         //缺省情况,派生类的func
    base::func();   //基类的func
}

一般来说,基类和派生类不定义同名成员变量

3.2 类的保护成员(protected)

基类的private成员:可以被下列函数访问
一基类的成员函数
一基类的友员函数
基类的public成员:可以被下列函数访问
一基类的成员函数
一基类的友员函数
—派生类的成员函数
—派生类的友员函数
一其他的函数
基类的protected成员:可以被下列函数访问
一基类的成员函数
一基类的友员函数
—派生类的成员函数可以访问当前对象的基类的保护成员

4、 派生类的构造函数

**在创建派生类的对象时,需要调用基类的构造函数:**初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。

class bug{
    private:
       int nlegs;
       int ncolor;
    public:
       int ntype;
       bug (int legs,int color);
       void printbug(){ };
};
class flybug:public bug{   //flybug是bug的派生类
    int nwings;
    public:
       flybug(int legs,int color,int wings);
};
bug::bug(int legs,int color){
    nlegs = legs;
    ncolor = color;
}
flybug::flybug(int legs,int color,int wings){    //错误的flybug构造函数
    nlegs = legs;     //error
    ncolor = color;   //error
    ntype = 1;   //ok
    nwings = wings;
}
flybug::flybug(int legs,int color,int wings):bug(legs,color){   //正确的flybug构造函数
    nwings = wings;
}
int main(){
    flybug fb(2,3,4);
    fb.printbug();
    fb.ntype = 1;
    fb.nlegs = 2;   //error,nlegs是私有成员
}

调用基类构造函数的两种方式
**显式方式:**在派生类的构造函数中, 为基类的构造函数提供参数
derived: :derived(arg_derived-list):base(arg_base-list)
**隐式方式:**在派生类的构造函数中, 省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数,
派生类的析构函数被执行时, 执行完派生类的析构函数后, 自动调用基类的析构函数。

class base{
    public:
    int n;
    base(int i):n(i)
    {cout << "base" << n << "constructed" << endl;}
    ~base()
    {cout << "base" << n << "destructed" << endl;}
};
class derived:public base{
    public:
       derived(int i):base(i)
       {cout << "derived constructed" << endl;}
       ~derived()
       {cout << "derived destructed" << endl;}
};
int main(){ derived obj(3);}

封闭派生类对象的构造函数执行顺序

析构函数的调用顺序与构造函数的调用顺序相反

在创建派生类的对象时:
1.先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
2.再执行成员对象类的构造函数,用以初始化派生类对象中成员对象;
3.最后再执行派生类自己的构造函数。

在派生类消亡时:
1.先执行派生类自己的析构函数
2.再依次执行基类的构造函数
3.最后再执行基类的析构函数

5、公有继承的赋值兼容规则

class base{};
class derived:public base{};
base b;
derived d;

1.派生类的对象可以赋值给基类对象:b = d;
2.派生类对象可以初始化基类引用:base & br = d;
3.派生类对象的地址可以赋值给基类指针:base pb = & d;*

如果派生方式是private或protected,则上述三条不可行

直接基类和间接基类 A —> B —> C
类A派生类B,类B派生类C
类A是类B的直接基类,类B是类C的直接基类,类A是类C的间接基类

在声明派生类时,只需要列出它的直接基类
派生类沿着类的层次自动向上继承它的间接基类
派生类的成员包括:
1.派生类自己定义的成员
2.直接基类中的所有成员
3.所有间接基类的全部成员

class base{
    public:
       int n;
    base(int i):n(i){
           cout << "base" << n << "constructed" << endl;
       }
    ~base(){
        cout << "base" << n << "destructed" << endl;
    }
};
class derived:public base{
    public:
       derived(int i):base(i){
           cout << "derived constructed" << endl;
       }
       ~derived(){
           cout << "derived destructed" << endl;
       }
};
class morederived:public derived{
    public:
       morederived():derived(4){
           cout << "more derived constructed" << endl;
       }
    ~morederived(){
        cout << "more derived destructed" << endl;
    }
};
int main(){
    morederived obj;
}

六、多态


1、虚函数和多态的基本概念

1.1 虚函数

​ 在类的定义中,前面有 virtual关键字的成员函数就是虚函数。
​ virtual关键字只用在类的定义里的函数声明中,写函数体时不用;构造函数和静态成员函数不能是虚函数

class base{
    virtual int get();
};
int base::get(){}

1.2 多态的表现形式

形式一:

​ 1.派生类的指针可以赋值给基类指针

​ 2.通过基类指针调用基类和派生类中的同名虚函数时:
​ (1)若该指针指向一个基类的对象,那么被调用是基类的虚函数
​ (2)若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数

​ 这种机制就叫做**“多态”**

class cbase{
    public:
    virtual void somefunc(){}
};
class cderived:public cbase{
    public:
    virtual void somefunc(){}
};
int main(){
    cderived i;
    cbase* p = & i;
    p->somefunc();   //调用哪个虚函数取决于P指向哪种类型的对象
}

形式二:

​ 1.派生类的对象可以赋给基类引用

​ 2.通过基类引用调用基类和派生类中的同名虚函数时:
​ (1)若该引用引用的是一个基类的对象,那么被调用是基类的虚函数
​ (2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数

​ 这种机制也叫做**“多态”**

class cbase{
    public:
    virtual void somefunc(){}
};
class cderived:public cbase{
    public:
    virtual void somefunc(){}
};
int main(){
    cderived i;
    cbase* r = & i;
    r.somefunc();//调用哪个虚函数取决于r引用哪种类型的对象
}

1.2 多态的实现原理

“ 多态” 的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定;这叫**“ 动态联编”**

多态实现的关键——虚函数表

class base{
    public:
    int i;
    virtual void print(){cout << "base" << endl;}
};
class derived:public base{
    public:
    int n;
    virtual void print(){cout << "derived" << endl;}
};
int main(){
    derived d;
    cout << sizeof(base) << "," << sizeof(derived);
}

每一个有虚函数的类( 或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的4 个字节就是用来放虚函数表的地址的

多态的函数调用语句被编译成一系列根据基类指针所指向的( 或基类引用所引用的) 对象中存放的虚函数表的地址, 在虚函数表中查找虚函数地址,并调用虚函数的指令。

2、虚析构函数

通过基类的指针删除派生类对象时, 通常情况下只调用基类的析构函数;但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。

解决办法: 把基类的析构函数声明为virtual
派生类的析构函数可以vi rtua 不进行声明
通过基类的指针删除派生类对象时, 首先调用派生类的析构函数, 然后调用基类的析构函数

一般来说, 一个类如果定义了虚函数, 则应该将析构函数也定义成虚函数。或者, 一个类打算作为基类使用, 也应该将析构函数定义成虚函数。
注意:不允许以虚函数作为构造函数

class son{
    public:
    ~son(){cout << "son" << endl;};
};
class grandson:public son{
    public:
    ~grandson(){cout << "grandson" << endl;};
};
int main(){
    son* pson;
    pson = new grandson();
    delete pson;
}  //输出son,没有执行grandson::~grandson()
class son{
    public:
       virtual ~son(){cout << "son" <

3、纯虚函数和抽象类

​ 纯虚函数:没有函数体的虚函数

class A{
    private: int a;
    public:
       virtual void print() = 0;   //纯虚函数,=0即表示没有函数体
       void fun(){ cout << "fun";}
};

抽象类:包含纯虚函数的类叫做抽象类

抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
抽象类的指针和引用可以指向由抽象类派生出来的类的对象

A  a;         //error,A是抽象类,不能创建对象
A * pa;       //ok,可以定义抽象类的指针和引用
pa = new A;   //error,A是抽象类,不能创建对象

在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。

如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。

class A{
    public:
    virtual void f() = 0; 
    void g( ){this->f();}
    A(){  //f();  error
       }
};
class B:public A{
    public:
       void f(){cout << "b:f()" << endl;}
};
int main(){
    B b;
    b.g();
}

七、输入和输出流


1、输入输出流相关的类

​ istream是用于输入的流类,cin就是该类的对象。

​ ostream是用于输出的流类,cout就是该类的对象。

​ ifstream是用于从文件读取数据的类。

​ ofstream是用于向文件写入数据的类。

​ iostream是既能用于输入,又能用于输出的类。

​ fstream 是既能从文件读取数据,又能向文件写入数据的类。

1.1 标准流对象

​ 输入流对象: cin 与标准输入设备相连 

​ 输出流对象:cout 与标准输出设备相连
​ cerr 与标准错误输出设备相连
​ clog 与标准错误输出设备相连

缺省情况下:
cerr << “Hello,world” << endl;
clog << “Hello,world” << endl;
和cout << “Hello,world” << endl; 一样

cin对应于标准输入流,用于从键盘读取数据,也可以被重定向 为从文件中读取数据。

cout对应于标准输出流,用于向屏幕输出数据,也可以被重定 向为向文件写入数据。

cerr对应于标准错误输出流,用于向屏幕输出出错信息,

clog对应于标准错误输出流,用于向屏幕输出出错信息,

cerr和clog的区别在于cerr不使用缓冲区,直接向显示器输出信 息;而输出到clog中的信息先会被存放在缓冲区,缓冲区满或者 刷新时才输出到屏幕。

1.2 判断输入流结束

可以用如下方法判输入流结束:

int x;
while (cin >> x){
    .......
}
//或者
istream &operator >>(int & a)
{
…….
return *this ;
}

如果是从文件输入,比如前面有 freopen(“some.txt”,”r”,stdin); 那么,读到文件尾部,输入流就算结束

如果从键盘输入,则在单独一行输入Ctrl+Z代表输入流结束

1.3 istream类的成员函数

istream & getline(char * buf, int bufSize);

从输入流中读取bufSize-1个字符到缓冲区buf,或读到碰到‘\n’ 为止(哪个先到算哪个)

istream & getline(char * buf, int bufSize,char delim);

从输入流中读取bufSize-1个字符到缓冲区buf,或读到碰到delim字 符为止(哪个先到算哪个)

**两个函数都会自动在buf中读入数据的结尾添加\0’ , ‘\n’或 delim都不会被读入buf,但会被从输入流中取走。**如果输入流中 ‘\n’或delim之前的字符个数达到或超过了bufSize个,就导致读 入出错,其结果就是:虽然本次读入已经完成,但是之后的读入就 都会失败了。

可以用 if(!cin.getline(…)) 判断输入是否结束

bool eof();  //判断输入流是否结束
int peek();  //返回下一个字符,但不从流中去掉
istream & putback(char c);  //将字符ch放回输入流
istream & ignore( int nCount = 1, int delim = EOF );   //从流中删掉最多nCount个字符,遇到EOF时结束
int main() {
int x;
char buf[100];
cin >> x;
cin.getline(buf,90);
cout << buf << endl;
return 0;
}
//输入:12 abcd↙
//输出:abcd (空格+abcd)
//输入:12↙
//程序立即结束,无输出:
//因为getline读到留在流中的’\n’就会返回

1.4 输出重定向

int main() {
int x,y;
cin >> x >> y;
freopen("test.txt","w",stdout); //将标准输出重定向到 test.txt文件
if( y == 0 ) //除数为0则在屏幕上输出错误信息
cerr << "error." << endl;
else
cout << x /y ; //输出结果到test.txt
return 0;
}
int main() {
double f; int n;
freopen(“t.txt”,“r”,stdin); //cin被改为从 t.txt中读取数据
cin >> f >> n;
cout << f << "," <

2、流操纵算子

2.1 概念

​ 整数流的基数:流操纵算子dec,oct,hex,setbase
​ 浮点数的精度(precision,setprecision
​ 设置域宽(setw,width)
​ 用户自定义的流操纵算子

使用流操纵算子需要 #include

//整数流的基数:流操纵算子dec,oct,hex
int n = 10;
cout << n << endl;
cout << hex << n << “\n”  //以十六进制输出
     << dec << n << “\n”  //以十进制输出
     << oct << n << endl; //以八进制输出

2.2 控制浮点数精度的流操纵算子

//precision是成员函数,其调用方式为:
cout.precision(5);
//setprecision 是流操作算子,其调用方式为:
cout << setprecision(5); // 可以连续输出

它们的功能相同

指定输出浮点数的有效位数(非定点方式输出时)
指定输出浮点数的小数点后的有效位数(定点方式输出时)
定点方式:小数点必须出现在个位数后面

int main()
{
double x = 1234567.89,y = 12.34567;
int n = 1234567;
int m = 12;
cout << setprecision(6) << x << endl  //浮点数最多输出六位有效数字
     << y << endl << n << endl << m;
} 
int main()
{
double x = 1234567.89,y = 12.34567;
int n = 1234567;
int m = 12;
cout << setiosflags(ios::fixed) <<   //以小数点位置固定的方式输出
     setprecision(6) << x << endl
     << y << endl << n << endl << m;
}
int main()
{
double x = 1234567.89;
cout << setiosflags(ios::fixed) <<
setprecision(6) << x << endl <<
resetiosflags(ios::fixed) << x ;  //取消以小数点位置固定的方式输出
}

2.3 设置域宽的流操纵算子

​ 设置域宽(setw,width) 两者功能相同,一个是成员函数,另一个是流操作 算子,调用方式不同:

cin >> setw(4); //或者 
cin.width(5);
cout << setw(4); //或者 
cout.width(5)
int w = 4;
char string[10];
cin.width(5);
while(cin >> string){
cout.width(w++);
cout << string << endl;
cin.width(5);
}  //宽度设置有效性是一次性的,在每次读入和输出之前都要设置宽度
int main() {
int n = 141;
//1) 分别以十六进制、十进制、八进制先后输出 n
cout << "1) " << hex << n << " " << dec << n << " " << oct << n << endl;
double x = 1234567.89,y = 12.34567;
//2) 保留5位有效数字
cout << "2) " << setprecision(5) << x << " " << y << " " << endl;
//3) 保留小数点后面5位
cout << "3) " << fixed << setprecision(5) << x << " " << y << endl ;
//4) 科学计数法输出,且保留小数点后面5位
cout << "4) " << scientific << setprecision(5) <

2.4 用户自定义流操作算子

ostream &tab(ostream &output){
return output << '\t';
}
cout << “aa” << tab << “bb” << endl;

以上代码能执行的原因:

*iostream 里对 << 进行了重载(成员函数) ostream & operator <<( ostream & ( * p ) ( ostream & ) ) ; 该函数内部会调用p所指向的函数,且以 this 作为参数 hex 、dec 、oct 都是函数

3、文件读写

3.1 文件和流

​ 可以将顺序文件看作一个有限字符构成的顺序字符流,然后像对 cin, cout 一样的读写。

包含在#include 头文件里面

3.2 创建文件

ofstream outFile(“clients.dat”, ios::out|ios::binary);
//创建文件

clients.dat” 要创建的文件的名字
ios::out 文件打开方式
ios:out 输出到文件, 删除原有内容
ios::app 输出到文件, 保留原有内容,总是在尾部添加
ios::binary 以二进制文件格式打开文件

也可以先创建ofstream对象,再用 open函数打开

ofstream fout;
fout.open("test.out",ios::out|ios::binary);
//判断打开是否成功:
if(!fout){
cout << “File open error!”<

文件名可以给出绝对路径,也可以给相对路径。没有交代路径信息, 就是在当前文件夹下找文件

3.3 文件名的绝对路径和相对路径

绝对路径:
"c:\tmp\mydir\some.txt"

相对路径:
"\tmp\mydir\some.txt" 当前盘符的根目录下的tmp\dir\some.txt
"tmp\mydir\some.txt" 当前文件夹的tmp子文件夹里面的……
"…\tmp\mydir\some.txt" 当前文件夹的父文件夹下面的tmp子文件夹里面的……
"…\…\tmp\mydir\some.txt" 当前文件夹的父文件夹的父文件夹下面的tmp子文件夹里面的……

3.4 文件的读写指针

对于输入文件,有一个读指针;
对于输出文件,有一个写指针;
对于输入输出文件,有一个读写指针;
标识文件操作的当前位置, 该指针在哪里,读写操 作就在哪里进行

ofstream fout("a1.out",ios::app); //以添加方式打开
long location = fout.tellp(); //取得写指针的位置
location = 10;
fout.seekp(location); // 将写指针移动到第10个字节处
fout.seekp(location,ios::beg); //从头数location
fout.seekp(location,ios::cur); //从当前位置数location
fout.seekp(location,ios::end); //从尾部数location

ifstream fin(“a1.in”,ios::ate);//打开文件,定位文件指针到文件尾
long location = fin.tellg(); //取得读指针的位置
location = 10L;
fin.seekg(location); // 将读指针移动到第10个字节处
fin.seekg(location,ios::beg); //从头数location
fin.seekg(location,ios::cur); //从当前位置数location
fin.seekg(location,ios::end); //从尾部数location

//location 可以为负值

3.5 显示关闭文件

ifstream fin(“test.dat”,ios::in);
fin.close();
ofstream fout(“test.dat”,ios::out);
fout.close();

3.6 字符文件读写

因为文件流也是流,所以流的成员函数和流操作算子也同样适 用于文件流。

写一个程序,将文件 in.txt 里面的整数排序后,输出到 out.txt

例如,若in.txt 的内容为: 1 234 9 45 6 879

则执行本程序后,生成的out.txt的内容为: 1 6 9 45 234 879

int main() {
vector v;
ifstream srcFile("in.txt",ios::in);
ofstream destFile("out.txt",ios::out);
int x;
while( srcFile >> x )
v.push_back(x);
sort(v.begin(),v.end());
for( int i = 0;i < v.size();i ++ )
destFile << v[i] << " ";
destFile.close();
srcFile.close();
return 0;
}

3.7 二进制文件的读写

二进制读文件:

ifstream 和 fstream的成员函数: istream& read (char s, long n);*

将文件读指针指向的地方的n个字节内容,读入到内存地址s,然后将文件读指针向后移动n字节 (以ios::in方式打开文件时,文件读指 针开始指向文件开头) 。

二进制写文件:

ofstream 和 fstream的成员函数: istream& write (const char s, long n);*

将内存地址s处的n个字节内容,写入到文件中写指针指向的位置, 然后将文件写指针向后移动n字节(以ios::out方式打开文件时,文件写指针开始指向文件开头, 以ios::app方式打开文件时,文件写 指针开始指向文件尾部 ) 。

// 在文件中写入和读取一个整数
int main() {
ofstream fout("some.dat", ios::out | ios::binary);
int x=120;
fout.write( (const char *)(&x), sizeof(int) );
fout.close();
ifstream fin("some.dat",ios::in | ios::binary);
int y;
fin.read((char * ) & y,sizeof(int));
fin.close();
cout << y <
//从键盘输入几个学生的姓名的成绩,并以二进制文件形式保存
struct Student {
char name[20];
int score;
};
int main() {
Student s;
ofstream OutFile( "c:\\tmp\\students.dat",ios::out|ios::binary);
while( cin >> s.name >> s.score )
OutFile.write( (char * ) & s, sizeof( s) );
OutFile.close();
return 0;
}
//students.dat 文件的内容读出并显示
struct Student {
char name[20];
int score;
};
int main() {
Student s;
ifstream inFile("students.dat",ios::in | ios::binary );
if(!inFile) {
cout << "error" <

3.8 文件拷贝程序mycopy

/*用法示例:
mycopy src.dat dest.dat
即将 src.dat 拷贝到 dest.dat 如果 dest.dat 原来就有,则原来的文件会被覆
盖 */
#include 
#include 
using namespace std;
int main(int argc, char * argv[])
{
if( argc != 3 ) {
cout << "File name missing!" << endl;
return 0;
}
ifstream inFile(argv[1],ios::binary|ios::in); //打开文件用
于读
if( ! inFile ) {
cout << "Source file open error." << endl;
return 0;
}
ofstream outFile(argv[2],ios::binary|ios::out); //打开文
件用于写
if( !outFile) {
cout << "New file open error." << endl;
inFile.close(); //打开的文件一定要关闭
return 0;
}
char c;
while( inFile.get(c)) //每次读取一个字符
outFile.put(c); //每次写入一个字符
outFile.close();
inFile.close();
return 0;
}

八、函数模板和类模板

1、函数模板

//交换两个整型变量的值的Swap函数:
void Swap(int & x,int & y) {
int tmp = x;
x = y;
y = tmp;
}
//交换两个double型变量的值的Swap函数:
void Swap(double & x,double & y) {
double tmp = x;
x = y;
y = tmp;
}

能否只写一个Swap, 就能交换各种类型的变量?

用函数模板解决:

template
返回值类型 模板名 (形参表) {
函数体 };

template 
void Swap(T & x,T & y) {
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1,m = 2;
Swap(n,m); //编译器自动生成 void Swap(int & ,int & )函数
double f = 1.2,g = 2.3;
Swap(f,g); //编译器自动生成 void Swap(double & ,double & )函数
}

函数模板中可以有不止一个类型参数

template 
T2 print(T1 arg1, T2 arg2)
{ 
cout<< arg1 << " "<< arg2<

编译器编译到这个模板的过程叫做模板的实例化

求数组最大元素的MaxElement函数模板

template 
T MaxElement(T a[], int size) //size是数组元素个数
{
T tmpMax = a[0];
for( int i = 1;i < size;++i)
if( tmpMax < a[i] )
tmpMax = a[i];
return tmpMax;
}

函数模板可以重载,只要它们的形参表或类型参数表不同即可

template
void print(T1 arg1, T2 arg2) { 
cout<< arg1 << " "<< arg2<
void print(T arg1, T arg2) { 
cout<< arg1 << " "<< arg2<
void print(T arg1, T arg2) { 
cout<< arg1 << " "<< arg2<

函数模板和函数的次序:

在有多个函数和函数模板名字相同的情况下,编译器如下处理一 条函数调用语句

  1. 先找参数完全匹配的**普通函数(**非由模板实例化而得的函数)
  2. 再找参数完全匹配的模板函数
  3. 再找实参数经过自动类型转换后能够匹配的普通函数
  4. 上面的都找不到,则报错。
template  
T Max( T a, T b) {
cout << "TemplateMax" < 
T Max( T a, T2 b) {
cout << "TemplateMax2" <

匹配模板函数时,不进行类型自动转换

template
T myFunction( T arg1, T arg2)
{ cout<

函数模板示例 Map

template
void Map(T s, T e, T x, Pred op)
{
for(; s != e; ++s,++x) {
*x = op(*s);
}
}
int Cube(int x) { return x * x * x; }
double Square(double x) { return x * x; }
int a[5] = {1,2,3,4,5}, b[5];
double d[5] = { 1.1,2.1,3.1,4.1,5.1} , c[5];
int main() {
Map(a,a+5,b,Square);
for(int i = 0;i < 5; ++i) cout << b[i] << ",";
cout << endl;
Map(a,a+5,b,Cube);
for(int i = 0;i < 5; ++i) cout << b[i] << ",";
cout << endl;
Map(d,d+5,c,Square);
for(int i = 0;i < 5; ++i) cout << c[i] << ",";
cout << endl;
return 0; }
template
void Map(T s, T e, T x, Pred op) {
for(; s != e; ++s,++x) {
*x = op(*s);
  }
}
int a[5] = {1,2,3,4,5}, b[5];
Map(a,a+5,b,Square); //实例化出以下函数:
void Map(int * s, int * e, int * x, double ( *op)(double)) {
for(; s != e; ++s,++x) {
*x = op(*s);
  }
}

2、类模板

为了多快好省地定义出一批相似的类,可以定义类模板,然后由类模 板生成不同的类

• 数组是一种常见的数据类型,元素可以是: – 整数 – 学生 – 字符串 – ……

• 考虑一个可变长数组类,需要提供的基本操作
– **len():**查看数组的长度
– **getElement(int index):**获取其中的一个元素
– **setElement(int index):**对其中的一个元素进行赋值

这些数组类,除了元素的类型不同之外,其他的完全相同

**类模板:**在定义类的时候,加上一个或多个类型参数。在使用类模板时,指定类型参数应该如何替换成具体类型,编译器据此生成相应的模板类。

template  //类型参数表
class 类模板名
{
//成员函数和成员变量
};
template 
//类型参数表
class 类模板名
{
//成员函数和成员变量
};

类模板里成员函数的写法:

template  //类型参数表
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表)
{ 
……
}
//用类模板定义对象的写法:
类模板名 <真实类型参数表> 对象名(构造函数实参表)

类模板示例:pair类模板

template 
class Pair
{
public:
T1 key; //关键字
T2 value; //值
Pair(T1 k,T2 v):key(k),value(v) { };
bool operator < ( const Pair & p) const; 
};
template
bool Pair::operator < ( const Pair & p) const 
//Pair的成员函数 operator <
{ 
return key < p.key; 
}
int main()
{
Pair student("Tom",19); 
//实例化出一个类 Pair
cout << student.key << " " << student.value; 
return 0;
}

编译器由类模板生成类的过程叫类模板的实例化。由类 模板实例化得到的类,叫模板类

同一个类模板的两个模板类是不兼容的

Pair * p;
Pair a;
p = & a; //error

函数模版作为类模板成员

template 
class A
{
public:
template 
void Func( T2 t) { cout << t; } //成员函数模板
};
int main() 
{
A a;
a.Func('K'); //成员函数模板 Func被实例化
a.Func("hello"); //成员函数模板 Func再次被实例化
return 0;
}

类模板与非类型参数
类模板的“<类型参数表>”中可以出现非类型参数:

template 
class CArray{
T array[size];
public:
void Print( ) 
{
for( int i = 0;i < size; ++i)
cout << array[i] << endl; 
  }
};
CArray a2;
CArray a3; //a2和a3属于不同的类

out << b[i] << “,”;
cout << endl;
Map(a,a+5,b,Cube);
for(int i = 0;i < 5; ++i) cout << b[i] << “,”;
cout << endl;
Map(d,d+5,c,Square);
for(int i = 0;i < 5; ++i) cout << c[i] << “,”;
cout << endl;
return 0; }


```c++
template
void Map(T s, T e, T x, Pred op) {
for(; s != e; ++s,++x) {
*x = op(*s);
  }
}
int a[5] = {1,2,3,4,5}, b[5];
Map(a,a+5,b,Square); //实例化出以下函数:
void Map(int * s, int * e, int * x, double ( *op)(double)) {
for(; s != e; ++s,++x) {
*x = op(*s);
  }
}

2、类模板

为了多快好省地定义出一批相似的类,可以定义类模板,然后由类模 板生成不同的类

• 数组是一种常见的数据类型,元素可以是: – 整数 – 学生 – 字符串 – ……

• 考虑一个可变长数组类,需要提供的基本操作
– **len():**查看数组的长度
– **getElement(int index):**获取其中的一个元素
– **setElement(int index):**对其中的一个元素进行赋值

这些数组类,除了元素的类型不同之外,其他的完全相同

**类模板:**在定义类的时候,加上一个或多个类型参数。在使用类模板时,指定类型参数应该如何替换成具体类型,编译器据此生成相应的模板类。

template  //类型参数表
class 类模板名
{
//成员函数和成员变量
};
template 
//类型参数表
class 类模板名
{
//成员函数和成员变量
};

类模板里成员函数的写法:

template  //类型参数表
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表)
{ 
……
}
//用类模板定义对象的写法:
类模板名 <真实类型参数表> 对象名(构造函数实参表)

类模板示例:pair类模板

template 
class Pair
{
public:
T1 key; //关键字
T2 value; //值
Pair(T1 k,T2 v):key(k),value(v) { };
bool operator < ( const Pair & p) const; 
};
template
bool Pair::operator < ( const Pair & p) const 
//Pair的成员函数 operator <
{ 
return key < p.key; 
}
int main()
{
Pair student("Tom",19); 
//实例化出一个类 Pair
cout << student.key << " " << student.value; 
return 0;
}

编译器由类模板生成类的过程叫类模板的实例化。由类 模板实例化得到的类,叫模板类

同一个类模板的两个模板类是不兼容的

Pair * p;
Pair a;
p = & a; //error

函数模版作为类模板成员

template 
class A
{
public:
template 
void Func( T2 t) { cout << t; } //成员函数模板
};
int main() 
{
A a;
a.Func('K'); //成员函数模板 Func被实例化
a.Func("hello"); //成员函数模板 Func再次被实例化
return 0;
}

类模板与非类型参数
类模板的“<类型参数表>”中可以出现非类型参数:

template 
class CArray{
T array[size];
public:
void Print( ) 
{
for( int i = 0;i < size; ++i)
cout << array[i] << endl; 
  }
};
CArray a2;
CArray a3; //a2和a3属于不同的类

你可能感兴趣的:(c++,数据结构,面向对象编程)