c++大学教程以及相关内容笔记整理

《C++大学教程》一本很详细的c++书籍,详细到基本可以当笔记来用  当然作为一个小白初学者还是整理一下笔记,顺便回顾。其中有一部分转载网上相关比较详细的内容

1 从C迁移到C++

c是c++的扩展以及增强;

不要假定没有数据类型就是int型,要显式的写出来(如:int main() );

初始化优于赋值(PITA规则);

在for循环中,计数变量可以在循环内声明(for(int i=0;i<5;++i));

在C++中,全局变量可以用表达式或者函数的调用来初始化,而不仅仅是使用常数;

不需要在编译时就知道数组的初始化值:(下面例子,变量a及b在堆栈中,编译时他们的值是未知的)

                  

void someFunction(void)
{
     int a=1,b=2;
     int someArray [] ={a,b};  //Error in C ,OK in C++
}
c++中的布尔类型,你可以这样写:

bool isGreater(int x,y)
{
    return x>y;
}
c++中不再允许Void *型指针进行隐式转换;

const说明某个对象是常量,并且所有常量必须初始化;

类型强制转换:

 static_cast关键字用来执行在不同的表示中要求”等价“的转换 (static_cast(expression)

const_cast显式的添加或取消对象的常量属性


2.命名空间

关键字 namespace

域解析运算符  :: 

//using声明
namespace A
{
    int x=1;
    ...
}
void someFunction()
{
    using A::x;
    int a=x;
    using A::  ...
    ...
}

   //using 指令
namespace A
{
    int x=1;
    ...
}
void someFunction()
{
    using namespace A;
    x=0;      //use A::x
   ...
}

using指令使用后,可以一劳永逸,对整个命名空间的所有成员都有效,非常方便。而using声明,则必须对命名空间的不同成员名称,一个一个地去声明,非常麻烦。

一般情况下,对偶尔使用的命名空间成员,应该使用命名空间的作用域解析运算符来直接给名称定位。而对一个大命名空间中的经常要使用的少数几个成员,提倡使用using声明,而不应该使用using编译指令。只有需要反复使用同一个命名空间的许多数成员时,使用using编译指令,才被认为是可取的。


3.输入输出基础

#include
using std::cout;
int main()
{
    cout<<4<<"ok"<>i>>d;
}

4.引用变量

引用变量即创建变量和对象的别名


#include
void addOne(int &arg)
{
    ++arg;
}
int main()
{
    int someVariable=0;
    addOne(someVariable);
    std::cout<

创建引用变量:

int data =0;
int &refData=data
所有引用都必须初始化,当需要支持常量对象或临时对象时,一定要使用const限定引用,但是不要用const限定引用名称本身。


5.动态内存分配

关键字new后面跟随一个类型,从而在空闲存储区中分配这种类型的单个对象的空间,关键字delete来释放由new分配的空间。

int *ptrInt1=new int;   //value is unknown
int *ptrInt2=new int();  //value is zero
double *ptrDouble=new double(3.1415);   //初始化基本类型
delete ptrInt1;  //释放
int *ptrInt=new int[5];    //为对象的数组创建空闲空间,在空闲空间中基本类型的数组不能被初始化
delete [] ptrInt;     //释放,一定用空[].

6.类

编写类定义:

class Circle
{
   //all data and member functions inside the Circle
};
C++的关键数据隐藏原则确保类的用户不能直接操作数据成员
访问限定符:

private:私有类成员只能被类的成员函数访问

public:共有类成员允许不受限制的被访问,作为类的公共接口

protected:对类的成员函数以及这个类的派生类的成员函数访问

class Circle
{
    public:
  //all public members
    private:
  //all private members
};
模块化举例:

//  File circle.h
#ifndef CIRCLE_H_INCLUDED
#define CIRCLE_H_INCLUDED

class Circle
{
public:
   
    void storeRadius(int newRadius);
    int getRadius() const;
    double getArea() const;
private:
    int radius;
};

#endif // CIRCLE_H_INCLUDED

//  File circle.cpp
#include "circle.h"
void Circle::storeRadius(int newRadius)
{
    if(newRadius>0)
        radius=newRadius;
}
int Circle::getRadius() const
{
    return radius;
}
double Circle::getArea() const
{
    double const pi=3.1415;
    return pi*radius*radius;

}


//   File main.cpp
#include 
#include "circle.h"
using namespace std;

int main()
{
    Circle real;
    real.storeRadius(6);
    cout<<"This radius is "<storeRadius(3);
    cout<<"this radius is "<getRadius()<getArea()<

内联函数:

  内联函数是其代码代替了通常生成的汇编语言“调用”的函数

  隐式内联函数是完全在类内部定义的函数

  显式内联函数出现在类定义之外,但是仍在类的头文件中。关键字inline必须在函数声明、定义、或者声明以及定义中出现

  当常量成员函数需要修改非静态数据成员时,使用关键字mutable

  枚举(enum):私有枚举,公有枚举



7.构造函数和析构函数

转载自:https://www.cnblogs.com/mr-wid/archive/2013/02/19/2917911.html
        构造函数主要用来在创建对象时完成对对象属性的一些初始化等操作, 当创建对象时, 对象会自动调用它的构造函数。一般来说, 构造函数有以下三个方面的作用:
            ■ 给创建的对象建立一个标识符;
            ■ 为对象数据成员开辟内存空间;
            ■ 完成对象数据成员的初始化。
        
 
        当用户没有显式的去定义构造函数时, 编译器会为类生成一个默认的构造函数, 称为 "默认构造函数", 默认构造函数不能完成对象数据成员的初始化, 只能给对象创建一标识符, 并为对象中的数据成员开辟一定的内存空间。
        
   
        无论是用户自定义的构造函数还是默认构造函数都主要有以下特点:
            ①. 在对象被创建时自动执行;
            ②. 构造函数的函数名与类名相同;
            ③. 没有返回值类型、也没有返回值;
            ④. 构造函数不能被显式调用。

构造函数的显式定义
    由于在大多数情况下我们希望在对象创建时就完成一些对成员属性的初始化等工作, 而默认构造函数无法满足我们的要求, 所以我们需要显式定义一个构造函数来覆盖掉默认构造函数以便来完成必要的初始化工作, 当用户自定义构造函数后编译器就不会再为对象生成默认构造函数。
    
    在构造函数的特点中我们看到, 构造函数的名称必须与类名相同, 并且没有返回值类型和返回值, 看一个构造函数的定义:

复制代码
 1     #include 
 2 
 3     using namespace std;
 4 
 5     class Point
 6     {
 7         public:
 8             Point()     //声明并定义构造函数
 9             {
10                 cout<<"自定义的构造函数被调用...\n";
11                 xPos = 100;         //利用构造函数对数据成员 xPos, yPos进行初始化
12                 yPos = 100;
13             }
14             void printPoint()
15             {
16                 cout<<"xPos = " << xPos <<endl;
17                 cout<<"yPos = " << yPos <<endl;
18             }
19 
20         private:
21             int xPos;
22             int yPos;
23     };
24 
25     int main()
26     {
27         Point M;    //创建对象M
28         M.printPoint();
29 
30         return 0;
31     }
复制代码

 
    编译运行的结果:

复制代码
        自定义的构造函数被调用...
        xPos = 100
        yPos = 100

        Process returned 0 (0x0)   execution time : 0.453 s
        Press any key to continue.
复制代码

 


    代码说明:
        在Point类的 public 成员中我们定义了一个构造函数Point() , 可以看到这个Point构造函数并不像 printPoint 函数有个void类型的返回值, 这正是构造函数的一特点。在构造函数中, 我们输出了一句提示信息, "自定义的构造函数被调用...", 并且将对象中的数据成员xPos和yPos初始化为100。
        
        在 main 函数中, 使用 Point 类创建了一个对象 M, 并调用M对象的方法 printPoint 输出M的属性信息, 根据输出结果看到, 自定义的构造函数被调用了, 所以 xPos和yPos 的值此时都是100, 而不是一个随机值。
        
        需要提示一下的是, 构造函数的定义也可放在类外进行。
        
        
        


        
三、有参数的构造函数
    在上个示例中实在构造函数的函数体内直接对数据成员进行赋值以达到初始化的目的, 但是有时候在创建时每个对象的属性有可能是不同的, 这种直接赋值的方式显然不合适。不过构造函数是支持向函数中传入参数的, 所以可以使用带参数的构造函数来解决该问题。
    

复制代码
 1     #include 
 2 
 3     using namespace std;
 4 
 5     class Point
 6     {
 7         public:
 8             Point(int x = 0, int y = 0)     //带有默认参数的构造函数
 9             {
10                 cout<<"自定义的构造函数被调用...\n";
11                 xPos = x;         //利用传入的参数值对成员属性进行初始化
12                 yPos = y;
13             }
14             void printPoint()
15             {
16                 cout<<"xPos = " << xPos <<endl;
17                 cout<<"yPos = " << yPos <<endl;
18             }
19 
20         private:
21             int xPos;
22             int yPos;
23     };
24 
25     int main()
26     {
27         Point M(10, 20);    //创建对象M并初始化xPos,yPos为10和20
28         M.printPoint();
29 
30         Point N(200);       //创建对象N并初始化xPos为200, yPos使用参数y的默认值0
31         N.printPoint();
32 
33         Point P;            //创建对象P使用构造函数的默认参数
34         P.printPoint();
35 
36         return 0;
37     }
复制代码

 
    编译运行的结果:

复制代码
        自定义的构造函数被调用...
        xPos = 10
        yPos = 20
        自定义的构造函数被调用...
        xPos = 200
        yPos = 0
        自定义的构造函数被调用...
        xPos = 0
        yPos = 0

        Process returned 0 (0x0)   execution time : 0.297 s
        Press any key to continue.
复制代码

 


    代码说明:
        在这个示例中的构造函数Point(int x = 0, int y = 0) 使用了参数列表并且对参数进行了默认参数设置为0。在 main 函数中共创建了三个对象 M, N, P。
            M对象不使用默认参数将M的坐标属性初始化10和20;
            N对象使用一个默认参数y, xPos属性初始化为200;
            P对象完全使用默认参数将xPos和yPos初始化为0。

            
            


            
三、构造函数的重载
    构造函数也毕竟是函数, 与普通函数相同, 构造函数也支持重载, 需要注意的是, 在进行构造函数的重载时要注意重载和参数默认的关系要处理好, 避免产生代码的二义性导致编译出错, 例如以下具有二义性的重载:
    

复制代码
        Point(int x = 0, int y = 0)     //默认参数的构造函数
        {
            xPos = x;
            yPos = y;
        }

        Point()         //重载一个无参构造函数
        {
            xPos = 0;
            yPos = 0;
        }
复制代码

       
    在上面的重载中, 当尝试用 Point 类重载一个无参数传入的对象 M 时, Point M; 这时编译器就报一条error: call of overloaded 'Point()' is ambiguous 的错误信息来告诉我们说 Point 函数具有二义性, 这是因为Point(int x = 0, int y = 0) 全部使用了默认参数, 即使我们不传入参数也不会出现错误, 但是在重载时又重载了一个不需要传入参数了构造函数Point(), 这样就造成了当创建对象都不传入参数时编译器就不知道到底该使用哪个构造函数了, 就造成了二义性。
    
    
    

 


四、初始化表达式
    对象中的一些数据成员除了在构造函数体中进行初始化外还可以通过调用初始化表来进行完成, 要使用初始化表来对数据成员进行初始化时使用: 号进行调出, 示例如下:
    

        Point(int x = 0, int y = 0):xPos(x), yPos(y)  //使用初始化表
        {
            cout<<"调用初始化表对数据成员进行初始化!\n";
        }

       
    在 Point 构造函数头的后面, 通过单个冒号: 引出的就是初始化表, 初始化的内容为 Point 类中int型的 xPos 成员和 yPos成员, 其效果和 xPos = x; yPos = y; 是相同的。
    
    与在构造函数体内进行初始化不同的是, 使用初始化表进行初始化是在构造函数被调用以前就完成的。每个成员在初始化表中只能出现一次, 并且初始化的顺序不是取决于数据成员在初始化表中出现的顺序, 而是取决于在类中声明的顺序。
    
    此外, 一些通过构造函数无法进行初始化的数据类型可以使用初始化表进行初始化, 如: 常量成员和引用成员, 这部分内容将在后面进行详细说明。使用初始化表对对象成员进行初始化的完整示例:
    

View Code

 



    


    
五、析构函数
    与构造函数相反, 析构函数是在对象被撤销时被自动调用, 用于对成员撤销时的一些清理工作, 例如在前面提到的手动释放使用 new 或 malloc 进行申请的内存空间。析构函数具有以下特点:
        ■ 析构函数函数名与类名相同, 紧贴在名称前面用波浪号 ~ 与构造函数进行区分, 例如:~Point();
        ■ 构造函数没有返回类型, 也不能指定参数, 因此析构函数只能有一个, 不能被重载;
        ■ 当对象被撤销时析构函数被自动调用, 与构造函数不同的是, 析构函数可以被显式的调用, 以释放对象中动态申请的内存。

 


    当用户没有显式定义析构函数时, 编译器同样会为对象生成一个默认的析构函数, 但默认生成的析构函数只能释放类的普通数据成员所占用的空间, 无法释放通过 new 或 malloc 进行申请的空间, 因此有时我们需要自己显式的定义析构函数对这些申请的空间进行释放, 避免造成内存泄露。

复制代码
 1 #include 
 2      #include 
 3 
 4      using namespace std;
 5 
 6      class Book
 7      {
 8          public:
 9              Book( const char *name )      //构造函数
10              {
11                  bookName = new char[strlen(name)+1];
12                  strcpy(bookName, name);
13              }
14              ~Book()                 //析构函数
15              {
16                  cout<<"析构函数被调用...\n";
17                  delete []bookName;  //释放通过new申请的空间
18              }
19              void showName() { cout<<"Book name: "<< bookName <<endl; }
20 
21          private:
22              char *bookName;
23      };
24 
25      int main()
26      {
27          Book CPP("C++ Primer");
28          CPP.showName();
29 
30          return 0;
31 
32      }
复制代码


    编译运行的结果:

        Book name: C++ Primer
        析构函数被调用...

        Process returned 0 (0x0)   execution time : 0.266 s
        Press any key to continue.


    代码说明:
        代码中创建了一个 Book 类, 类的数据成员只有一个字符指针型的 bookName, 在创建对象时系统会为该指针变量分配它所需内存, 但是此时该指针并没有被初始化所以不会再为其分配其他多余的内存单元。在构造函数中, 我们使用 new 申请了一块 strlen(name)+1 大小的空间, 也就是比传入进来的字符串长度多1的空间, 目的是让字符指针 bookName 指向它, 这样才能正常保存传入的字符串。
        
        在 main 函数中使用 Book 类创建了一个对象 CPP, 初始化 bookName 属性为 "C++ Primer"。从运行结果可以看到, 析构函数被调用了, 这时使用 new 所申请的空间就会被正常释放。
        
        自然状态下对象何时将被销毁取决于对象的生存周期, 例如全局对象是在程序运行结束时被销毁, 自动对象是在离开其作用域时被销毁。
        
    如果需要显式调用析构函数来释放对象中动态申请的空间只需要使用 对象名.析构函数名(); 即可, 例如上例中要显式调用析构函数来释放 bookName 所指向的空间只要:

        CPP.~Book();


复制构造函数

转自:https://www.cnblogs.com/raichen/p/4752025.html


几个原则:

C++ primer p406 :复制构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用复制构造函数。

 

C++支持两种初始化形式:复制初始化(int a = 5;)和直接初始化(int a(5);)对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,复制初始化总是调用复制构造函数,也就是说:

A x(2);  //直接初始化,调用构造函数
A y = x;  //复制初始化,调用复制构造函数

 

必须定义复制构造函数的情况:

只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数也可以复制;有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义复制构造函数。

 

什么情况使用复制构造函数:

类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体
(2)一个对象以值传递的方式从函数返回
(3)一个对象需要通过另外一个对象进行初始化。

 

深拷贝和浅拷贝:

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间

如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝

上面提到,如果没有自定义复制构造函数,则系统会创建默认的复制构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。下面是示例:

复制代码
 1 #include 
 2 #include <string.h>
 3 class Person 
 4 {
 5 public :
 6          
 7     // 构造函数
 8     Person(char * pN)
 9     {
10         cout << "一般构造函数被调用 !\n";
11         m_pName = new char[strlen(pN) + 1];
12         //在堆中开辟一个内存块存放pN所指的字符串
13         if(m_pName != NULL) 
14         {
15            //如果m_pName不是空指针,则把形参指针pN所指的字符串复制给它
16              strcpy(m_pName ,pN);
17         }
18     }        
19        
20     // 系统创建的默认复制构造函数,只做位模式拷贝
21     Person(Person & p)    
22     { 
23         //使两个字符串指针指向同一地址位置         
24         m_pName = p.m_pName;         
25     }
26  
27     ~Person( )
28     {
29         delete m_pName;
30     }
31          
32 private :
33     char * m_pName;
34 };
35  
36 void main( )
37 { 
38     Person man("lujun");
39     Person woman(man); 
40      
41     // 结果导致   man 和    woman 的指针都指向了同一个地址
42      
43     // 函数结束析构时
44     // 同一个地址被delete两次
45 }
46  
47  
48 // 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
49 Person(Person & chs);
50 {
51      // 用运算符new为新对象的指针数据成员分配空间
52      m_pName=new char[strlen(p.m_pName)+ 1];
53  
54      if(m_pName)         
55      {
56              // 复制内容
57             strcpy(m_pName ,chs.m_pName);
58      }
59    
60     // 则新创建的对象的m_pName与原对象chs的m_pName不再指向同一地址了
61 }
复制代码

 

重载赋值操作符:

通过定义operate=的函数,可以对赋值进行定义。像其他任何函数一样,操作符函数有一个返回值和形参表。形参表必须具有与该操作符操作数书目相同的形参(如果操作符是一个成员,则包括隐式this形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参(隐含的this指针)对应着左操作数,第二个形参对应右操作数。

 一个应用了对赋值号重载的拷贝构造函数的例子:

复制代码
 1 #include 
 2 
 3 using namespace std;
 4 
 5 class A
 6 {
 7 public:
 8     A(int);//构造函数
 9     A(const A &);//拷贝构造函数
10     ~A();
11     void print();
12     int *point;
13     A &operator=(const A &);
14 };
15 
16 A::A(int p)
17 {
18     point = new int;
19     *point = p;
20 }
21 
22 A::A(const A &b)
23 {
24     *this = b;
25     cout<<"调用拷贝构造函数"<<endl;
26 }
27 
28 A::~A()
29 {
30     delete point;
31 }
32 
33 void A::print()
34 {
35     cout<<"Address:"<" value:"<<*point<<endl;
36 }
37 
38 A &A::operator=(const A &b)
39 {
40     if( this != &b)
41     {
42         delete point;
43         point = new int;
44         *point = *b.point;
45     }
46 }
47 
48 
49 int main()
50 {
51     A x(2);
52     A y = x;
53     x.print();
54     delete x.point;
55     y.print();
56 
57     return 0;
58 }
复制代码

 


8.类的其他特征

this指针是每个非静态成员函数中的第一个隐藏参数。当调用函数时,该指针将被初始化为指向调用对象

在成员函数中,编写代码*this就可以获得当前调用对象本身

如果一个成员函数返回*this,那么可以把一个成员函数和另一个成员函数链接到一起,并且通过引用将其类名指定为返回类型

类的静态数据成员是实例无关的,必须用关键字static声明,并且在类的定义文件中定义

由于C++的封装性和数据隐藏性,只有类自身的函数可以不受限制的访问类中的非公有成员,而通过友元函数可以直接访问类的非公有成员函数

可以在类定义中的任意位置用关键字 friend来声明类的友元函数


.....


9.异常处理

简单示例:

#include
using std::cout;
using std::cin;
using std::cerr;

double getQuotient(int numerator,int denominator)
{
     if (denominator==0)
         throw "Zero denominator";
     retyrn static_cast(number)/denominator;
}

int main()
{
     cout<<"Enter a numerator and denominator:  ";
     int num,den;
     while(!(cin>>num>>den).eof())
     {
        try
        {
            cout>>"Quotient is"<

10.函数重载

转自:http://blog.csdn.net/zhanghow/article/details/53588458

在实际开发中,有时候我们需要实现几个功能类似的函数,只是有些细节不同。例如希望交换两个变量的值,这两个变量有多种类型,可以是 int、float、char、bool 等,我们需要通过参数把变量的地址传入函数内部。在C语言中,程序员往往需要分别设计出三个不同名的函数,其函数原型与下面类似:
  1. voidswap1(int*a,int*b);//交换 int 变量的值
  2. voidswap2(float*a,float*b);//交换 float 变量的值
  3. voidswap3(char*a,char*b);//交换 char 变量的值
  4. voidswap4(bool*a,bool*b);//交换 bool 变量的值
但在C++中,这完全没有必要。C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是 函数的重载(Function Overloading) 。借助重载,一个函数名可以有多种用途。

参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。

【示例】借助函数重载交换不同类型的变量的值:
  1. #include
  2. usingnamespace std;

  3. //交换 int 变量的值
  4. voidSwap(int*a,int*b){
  5. int temp=*a;
  6. *a=*b;
  7. *b= temp;
  8. }

  9. //交换 float 变量的值
  10. voidSwap(float*a,float*b){
  11. float temp=*a;
  12. *a=*b;
  13. *b= temp;
  14. }

  15. //交换 char 变量的值
  16. voidSwap(char*a,char*b){
  17. char temp=*a;
  18. *a=*b;
  19. *b= temp;
  20. }

  21. //交换 bool 变量的值
  22. voidSwap(bool*a,bool*b){
  23. char temp=*a;
  24. *a=*b;
  25. *b= temp;
  26. }

  27. intmain(){
  28. //交换 int 变量的值
  29. int n1=100, n2= 200;
  30. Swap(&n1,&n2);
  31. cout<<n1<<", "<<n2<<endl;

  32. //交换 float 变量的值
  33. float f1=12.5, f2= 56.93;
  34. Swap(&f1,&f2);
  35. cout<<f1<<", "<<f2<<endl;

  36. //交换 char 变量的值
  37. char c1='A', c2= 'B';
  38. Swap(&c1,&c2);
  39. cout<<c1<<", "<<c2<<endl;

  40. //交换 bool 变量的值
  41. bool b1=false, b2= true;
  42. Swap(&b1,&b2);
  43. cout<<b1<<", "<<b2<<endl;

  44. return0;
  45. }
运行结果:
200, 100
56.93, 12.5
B, A
1, 0

本例之所以使用 Swap 这个函数名,而不是使用 swap ,是因为 C++ 标准库已经提供了交换两个变量的值的函数,它的名字就是 swap ,位于 algorithm 头文件中,为了避免和标准库中的 swap 冲突,本例特地将 S 大写。

既然标准库已经提供了 swap() 函数,本例为何又要自己实现一遍呢,这不是费力不讨好吗?交换两个变量的值是一个经典且实用的函数重载案例,本例这样做仅仅是为了教学演示,并不是要替代标准库中的 swap(),读者在以后的编码过程中也应该坚持使用标准库中的 swap()。

通过本例可以发现,重载就是在一个作用范围内(同一个类、同一个命名空间等)有多个名称相同但参数不同的函数。重载的结果是让一个函数名拥有了多种用途,使得命名更加方便(在中大型项目中,给变量、函数、类起名字是一件让人苦恼的问题),调用更加灵活。

在使用重载函数时,同名函数的功能应当相同或相近,不要用同一函数名去实现完全不相干的功能,虽然程序也能运行,但可读性不好,使人觉得莫名其妙。

注意,参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数返回值也不能作为重载的依据。

函数的重载的规则:
  • 函数名称必须相同。
  • 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
  • 函数的返回类型可以相同也可以不相同。
  • 仅仅返回类型不同不足以成为函数的重载。

C++ 是如何做到函数重载的

C++代码在编译时会根据参数列表对函数进行重命名,例如 void Swap(int a, int b) 会被重命名为 _Swap_int_int void Swap(float x, float y) 会被重命名为 _Swap_float_float 。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做 重载决议( Overload Resolution
不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。
从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样


11.继承

转自:https://www.cnblogs.com/metalsteel/p/6280389.html

一,继承的基本概念

1.类与类之间的关系

  • has-A,包含关系,用以描述一个类由多个“部件类”构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类。
  • use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现。
  • is-A,即继承关系,关系具有传递性。

2.继承的相关概念

  万事万物皆有继承这个现象,所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。

3.继承的特点

  • 子类拥有父类的所有属性和方法(除了构造函数和析构函数)。
  • 子类可以拥有父类没有的属性和方法。
  • 子类是一种特殊的父类,可以用子类来代替父类。
  • 子类对象可以当做父类对象使用。

4.继承的语法

  c++大学教程以及相关内容笔记整理_第1张图片

 二,继承中的访问控制

1.继承的访问控制方式

  我们在一个类中可以对成员变量和成员函数进行访问控制,通过C++提供的三种权限修饰符实现。在子类继承父类时,C++提供的三种权限修饰符也可以在继承的时候使用,分别为公有继承,保护继承和私有继承。这三种不同的继承方式会改变子类对父类属性和方法的访问。

2.三种继承方式对子类访问的影响

  • public继承:父类成员在子类中保持原有的访问级别(子类可以访问public和protected)。
  • private继承:父类成员在子类中变为private成员(虽然此时父类的成员在子类中体现为private修饰,但是父类的public和protected是允许访问的,因为是继承后改为private)。
  • protected继承
    • 父类中的public成员会变为protected级别。
    • 父类中的protected成员依然为protected级别。
    • 父类中的private成员依然为private级别。
  • 注意:父类中的private成员依然存在于子类中,但是却无法访问到。不论何种方式继承父类,子类都无法直接使用父类中的private成员。

3.父类如何设置子类的访问

  • 需要被外界访问的成员设置为public。
  • 只能在当前类中访问设置为private。
  • 只能在当前类和子类中访问,设置为protected。

三,继承中的构造和析构函数

1.父类的构造和析构

  当创建一个对象和销毁一个对象时,对象的构造函数和析构函数会相应的被C++编译器调用。当在继承中,父类的构造和析构函数是如何在子类中进行调用的呢,C++规定我们在子类对象构造时,需要调用父类的构造函数完成对对继承而来的成员进行初始化,同理,在析构子类对象时,需要调用父类的析构函数对其继承而来的成员进行析构。

2.父类中的构造和析构执行顺序

  • 子类对象在创建时,会先调用父类的构造函数,如果父类还存在父类,则先调用父类的父类的构造函数,依次往上推理即可。
  • 父类构造函数执行结束后,执行子类的构造函数。
  • 当父类的构造函数不是C++默认提供的,则需要在子类的每一个构造函数上使用初始化列表的方式调用父类的构造函数。
  • 析构函数的调用顺序和构造函数的顺序相反。

3.继承中的构造函数示例

复制代码
# include
using namespace std;

class Parent
{
protected:
    char * str;
public:
    Parent(char * str)
    {
        if (str != NULL)
        {
            this->str = new char[strlen(str) + 1];
            strcpy(this->str, str);
        }
        else {
            this->str = NULL;
        }
        cout << "父类构造函数..." << endl;
    }
    ~Parent()
    {
        if (this->str != NULL)
        {
            delete[] this->str;
            this->str = NULL;
        }
        cout << "父类析构函数..." << endl;
    }
};

class Child:public Parent
{
private:
    char * name;
    int age;
public:
    /* 在构造子类对象时,调用父类的构造函数 */
    Child():Parent(NULL)
    {
        this->name = NULL;
        this->age = 0;
        cout << "子类无参构造函数..." << endl;
    }
    /* 在构造子类对象时,调用父类的构造函数 */
    Child(char * name,int age):Parent(name)
    {
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        cout << "子类有参构造函数..." << endl;
    }
    ~Child()
    {
        if (this->name != NULL)
        {
            delete[] this->name;
            this->name = NULL;
        }
        cout << "子类析构函数..." << endl;
    }
};

int main()
{
    Child c1;
    Child c2("王刚",22);

    return 0;
}
复制代码

输出结果:

c++大学教程以及相关内容笔记整理_第2张图片

4.继承与组合情况混搭下的构造析构函数调用顺序

  • 构造函数:先调用父类的构造函数,再调用成员变量的构造函数,最后调用自己的构造函数。
  • 析构函数:先调用自己的析构函数,再调用成员变量的析构函数,最后调用父类的析构函数。

5.继承和组合情况混搭情况下的代码演示

复制代码
# include
using namespace std;
/* 定义父类 */
class Parent
{
protected:
    char * str;
public:
    Parent(char * str)
    {
        if (str != NULL)
        {
            this->str = new char[strlen(str) + 1];
            strcpy(this->str, str);
        }
        else {
            this->str = NULL;
        }
        cout << "父类构造函数..." << endl;
    }
    ~Parent()
    {
        if (this->str != NULL)
        {
            delete[] this->str;
            this->str = NULL;
        }
        cout << "父类析构函数..." << endl;
    }
};
/* 定义Object类 */
class Object
{
private:
    int i;
public:
    Object(int i)
    {
        this->i = i;
        cout << "Object的构造函数..." << endl;
    }
    ~Object()
    {
        cout << "Object的析构函数..." << endl;
    }
};
/* 定义子类 */
class Child:public Parent
{
private:
    /* 定义类成员变量 */
    Object obj;
    char * name;
    int age;
public:
    /* 在构造子类对象时,调用父类的构造函数和调用成员变量的构造函数 */
    Child():Parent(NULL),obj(10)
    {
        this->name = NULL;
        this->age = 0;
        cout << "子类无参构造函数..." << endl;
    }
    /* 在构造子类对象时,调用父类的构造函数和调用成员变量的构造函数 */
    Child(char * name,int age):Parent(name),obj(age)
    {
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        cout << "子类有参构造函数..." << endl;
    }
    ~Child()
    {
        if (this->name != NULL)
        {
            delete[] this->name;
            this->name = NULL;
        }
        cout << "子类析构函数..." << endl;
    }
};

int main()
{
    Child c1;
    Child c2("王刚",22);

    return 0;
}
复制代码

输出结果:

c++大学教程以及相关内容笔记整理_第3张图片

四,继承中的类型兼容性原则

1.继承中的同名成员

  当在继承中,如果父类的成员和子类的成员属性名称相同,我们可以通过作用域操作符来显式的使用父类的成员,如果我们不使用作用域操作符,默认使用的是子类的成员属性。

2.继承中的同名成员演示

复制代码
# include
using namespace std;

class PP
{
public:
    int i;
};
class CC:public PP
{
public:
    int i;
public:
    void test()
    {
        /* 使用父类的同名成员 */
        PP::i = 10;
        /* 使用子类的同名成员 */
        i = 100;
    }
    void print()
    {
        cout << "父类:" << PP::i << "," << "子类:" << i << endl;
    }
};

int main()
{
    CC cc;
    cc.test();
    cc.print();
    return 0;
}
复制代码

3.类型兼容性原则

  类型兼容性原则是指在需要父类对象的所有地方,都可以用公有继承的子类对象来替代。通过公有继承,子类获得了父类除构造和析构之外的所有属性和方法,这样子类就具有了父类的所有功能,凡是父类可以解决的问题,子类也一定可以解决。

4.类型兼容性原则可以替代的情况

  • 子类对象可以当做父类对象来使用。
  • 子类对象可以直接赋值给父类对象。
  • 子类对象可以直接初始化父类对象。
  • 父类指针可以直接指向子类对象。
  • 父类引用可以直接引用子类对象。

5.类型兼容性原则示例

复制代码
# include
using namespace std;
/* 创建父类 */
class MyParent
{
protected:
    char * name;
public:
    MyParent()
    {
        name = "HelloWorld";
    }
    void print()
    {

        cout << "name = " << name << endl;
    }
};
/* 创建子类 */
class MyChild:public MyParent
{
protected:
    int i;
public:
    MyChild()
    {
        i = 100;
        name = "I am Child";
    }
};

void main()
{
    /* 定义子类对象 */
    MyChild c;
    /* 用子类对象当做父类对象使用 */
    c.print();
    /* 用子类对象初始化父类对象 */
    MyParent p1 = c;
    p1.print();
    /* 父类指针直接指向子类对象 */
    MyParent * p2 = &c;
    p2->print();
    /* 父类对象直接引用子类对象 */
    MyParent& p3 = c;
    p3.print();
}
复制代码

6.继承中的static

  • 继承中的static也遵循三种继承的基本访问控制原则。
  • 继承中的static可以通过类名和域作用符的方式访问,也可以通过对象点的方式访问。

7.继承中的static演示

复制代码
# include

using namespace std;
/* 父类 */
class MyP
{
public:
    static int i;
};
/* 初始化静态成员 */
int MyP::i = 10;
/* 子类 */
class MyC:public MyP
{
public:
    void test()
    {
        /* 直接访问 */
        cout << "i = " << i << endl;
        /* 通过父类访问 */
        cout << "Myp::i = " << MyP::i << endl;
        /* 通过子类访问 */
        cout << "MyC::i = " << MyC::i << endl;
    }
    void add()
    {
        i++;
    }
};
int main()
{
    MyC c;
    c.add();
    c.test();

    MyC c1;
    c1.add();
    c1.test();
    /* 通过子类对象访问 */
    c1.i = 100;
    c1.test();

    return 0;
}
复制代码

五,多继承

1.C++中的多继承

  所谓的多继承就是指一个子类可以继承多个父类,子类可以获取多个父类的属性和方法。这种继承方式是不被推荐的,但是C++还是添加了,事实证明,多继承增加了代码的复杂度,而且任何可以通过多继承解决的问题都可以通过单继承的方式解决。多继承和单继承的基本知识是相同的。不需要再阐述,主要讲解下面的不同的地方。

2,多继承中的构造和析构

  和单继承类似,还是首先执行父类的构造函数,此时有多个构造函数,则按照继承时的父类顺序来执行相应父类的构造函数,析构函数与此相反。

3.多继承中的二义性

  一个类A,它有两个子类B1和B2,然后类C多继承自B1和B2,此时如果我们使用类A里面的属性,则根据上面的多继承的构造和析构,发现此时的类A会被创造两个对象,一个是B1一个是B2,此时使用A里面的属性则会出现编译器无法知道是使用B1的还是B2的。因此C++为我们提供了虚继承这个概念,即B1和B2虚继承自A,则在构造A对象的时候,只创建一个A的对象。

4.多继承的二义性代码示例

  此时如果去除virtual关键字,尝试一下会报错。

复制代码
# include
using namespace std;
/* 类B */
class B
{
public:
    int a;
};
/* 虚继承自类B */
class B1 :virtual public B
{

};
/* 虚继承自类B */
class B2 :virtual public B
{

};
/* 继承自B1,B2 */
class C :public B1, public B2
{
};

void main()
{
    C c;
    c.B::a = 100;

    cout << c.B::a << endl;
    cout << c.B1::a << endl;
    cout << c.B2::a << endl;
}
复制代码

12.模板

1.模板的概念。

我们已经学过重载(Overloading),对重载函数而言,C++的检查机制能通过函数参数的不同及所属类的不同。正确的调用重载函数。例如,为求两个数的最大值,我们定义MAX()函数需要对不同的数据类型分别定义不同重载(Overload)版本。

//函数1.

int max(int x,int y);
{return(x>y)?x:y ;}

//函数2.
float max( float x,float y){
return (x>y)? x:y ;}

//函数3.
double max(double x,double y)
{return (c>y)? x:y ;}

但如果在主函数中,我们分别定义了 char a,b; 那么在执行max(a,b);时 程序就会出错,因为我们没有定义char类型的重载版本。

现在,我们再重新审视上述的max()函数,它们都具有同样的功能,即求两个数的最大值,能否只写一套代码解决这个问题呢?这样就会避免因重载函数定义不 全面而带来的调用错误。为解决上述问题C++引入模板机制,模板定义:模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。模版可以分为两类,一个是函数模版,另外一个是类模版。

2.  函数模板的写法

函数模板的一般形式如下:

Template 或者也可以用typename T>

返回类型 函数名(形参表)
{//
函数定义体 }

说明: template是一个声明模板的关键字,表示声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型可以包含类类型.

请看以下程序:

//Test.cpp

#include

using std::cout;

using std::endl;

//声明一个函数模版,用来比较输入的两个相同数据类型的参数的大小,class也可以被typename代替,

//T可以被任何字母或者数字代替。

template <class T>

T min(T x,T y)

{return(x

void main( )

{

    int n1=2,n2=10;

    double d1=1.5,d2=5.6;

    cout<<"较小整数:"<

    cout<<"较小实数:"<

    system("PAUSE");

}

程序运行结果: 

 

程序分析:main()函数中定义了两个整型变量n1 , n2 两个双精度类型变量d1 , d2然后调用min( n1, n2);即实例化函数模板T min(T x, T y)其中T为int型,求出n1,n2中的最小值.同理调用min(d1,d2)时,求出d1,d2中的最小值.

3.类模板的写法

定义一个类模板:

Template < class或者也可以用typename T >
class类名{
//类定义......
};

说明:其中,template是声明各模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个。

例如:定义一个类模板:

// ClassTemplate.h
#ifndef ClassTemplate_HH

#define ClassTemplate_HH

template<typename T1,typename T2>

class myClass{

private:

    T1 I;

    T2 J;

public:

    myClass(T1 a, T2 b);//Constructor

    void show();

};

//这是构造函数

//注意这些格式

template <typename T1,typename T2>

myClass::myClass(T1 a,T2 b):I(a),J(b){}

//这是void show();

template <typename T1,typename T2>

void myClass::show()

{

    cout<<"I="<", J="<

}

#endif

// Test.cpp

#include

#include"ClassTemplate.h"

using std::cout;

using std::endl;

void main()

{

    myClass<int,int> class1(3,5);

    class1.show();

    myClass<int,char> class2(3,"a");

    class2.show();

    myClass<double,int> class3(2.9,10);

    class3.show();

    system("PAUSE");

}

最后结果显示:

 

4.非类型模版参数

一般来说,非类型模板参数可以是常整数(包括枚举)或者指向外部链接对象的指针。

那么就是说,浮点数是不行的,指向内部链接对象的指针是不行的。


template<typename T,int MAXSIZE>

class Stack{

Private:

      T elems[MAXSIZE];

};

Int main()

{

      Stack<int, 20> int20Stack;

      Stack<int, 40> int40Stack;

};

 


关于C++Template的理解:
C++Template包含函数模板和类模板两种,顾名思义这两种的不同点在于所使用的场合不同,函数模板针对于函数使用,而类模板则针对于类使用.
使用类模板有什么优势呢?现在你要写一个求最大数的方法,你考虑到了N种情况(求两个整数,求两个浮点数,求两个字符,求...),这时候你可能会写N个重载方法,当你缴尽脑汁把一切可能发生的事情都想到的时候这个程序写完了,代码量是很多的,这时候解决这个问题的一个好的办法就是使用C++的模板,我们可以定义一个函数模板,这个模板接收任意类型的参数,编译器会根据你所传入的参数类型编译成对应类型的函数,及对应类型的返回值.
模板,在我的理解中就是定义一个公共的需求,比如这个word文档的模板,定义了大家都有可能用到的样式,C++的模板也就是让你定义一个公共的模块,把一些类似的功能的模块归类为一个模板,使用模板到底有什么好处呢?我觉的可以提高程序的重用性,减少代码的冗余及代码量

  





  关键字typeid

转自:http://www.cppblog.com/smagle/archive/2010/05/14/115286.html

在揭开typeid神秘面纱之前,我们先来了解一下RTTI(Run-Time Type Identification,运行时类型识别),它使程序能够获取由基指针或引用所指向的对象的实际派生类型,即允许“用指向基类的指针或引用来操作对象”的程序能够获取到“这些指针或引用所指对象”的实际派生类型。在C++中,为了支持RTTI提供了两个操作符:dynamic_cast和typeid。
    dynamic_cast允许运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转化类型,与之相对应的还有一个非安全的转换操作符static_cast,因为这不是本文的讨论重点,所以这里不再详述,感兴趣的可以自行查阅资料。下面就开始今天我们的话题:typeid。
   
    typeid是C++的关键字之一,等同于sizeof这类的操作符。typeid操作符的返回结果是名为type_info的标准库类型的对象的引用(在头文件typeinfo中定义,稍后我们看一下vs和gcc库里面的源码),它的表达式有下图两种形式。
   
   
    如果表达式的类型是类类型且至少包含有一个虚函数,则typeid操作符返回表达式的动态类型,需要在运行时计算;否则,typeid操作符返回表达式的静态类型,在编译时就可以计算。
    ISO C++标准并没有确切定义type_info,它的确切定义编译器相关的,但是标准却规定了其实现必需提供如下四种操作(在之后的章节中我会来分析type_info类文件的源码):

 t1 == t2  如果两个对象t1和t2类型相同,则返回true;否则返回false
 t1 != t2  如果两个对象t1和t2类型不同,则返回true;否则返回false
 t.name()  返回类型的C-style字符串,类型名字用系统相关的方法产生
 t1.before(t2)  返回指出t1是否出现在t2之前的bool值
    type_info类提供了public虚析构函数,以使用户能够用其作为基类。它的默认构造函数和拷贝构造函数及赋值操作符都定义为private,所以不能定义或复制type_info类型的对象。程序中创建type_info对象的唯一方法是使用typeid操作符(由此可见,如果把typeid看作函数的话,其应该是type_info的友元)。type_info的name成员函数返回C-style的字符串,用来表示相应的类型名,但务必注意这个返回的类型名与程序中使用的相应类型名并不一定一致(往往如此,见后面的程序),这 具体由编译器的实现所决定的,标准只要求实现为每个类型返回唯一的字符串。

    上面的都是一些理论的东西,看不真切,下面将通过代码和图例来展示。
    

#include  < iostream >
using   namespace  std;

class  Base {};
class  Derived:  public  Base {};

int  main()
{
    Base b, *pb;
    pb = NULL;
    Derived d;

    cout 
<<  typeid( int ).name()  <<  endl
         
<<  typeid(unsigned).name()  <<  endl
         
<<  typeid( long ).name()  <<  endl
         
<<  typeid(unsigned  long ).name()  <<  endl
         
<<  typeid( char ).name()  <<  endl
         
<<  typeid(unsigned  char ).name()  <<  endl
         
<<  typeid( float ).name()  <<  endl
         
<<  typeid( double ).name()  <<  endl
         
<<  typeid( string ).name()  <<  endl
         
<<  typeid(Base).name()  <<  endl
        
<< typeid(b).name()<          << typeid(pb).name()<          << typeid(Derived).name() << endl
         << typeid(d).name()<
         
<<  typeid(type_info).name()  <<  endl;
         
    
return   0 ;
}

    我分别用MS的V8和GUN的GCC编译该段代码并运行,结果分别为下面的左右二图。
    c++大学教程以及相关内容笔记整理_第4张图片  c++大学教程以及相关内容笔记整理_第5张图片
    对比代码以及上面的文字描述,不知道各位是否已经有所明了(这里需要注意的是Base类的对象b和对象指针pb,他们的输出)。
    考虑到V8的输出很直观,所以我采用V8来做实验。下面对上面的代码稍微添加一点内容,如下:
    
Base  * pb2  =  dynamic_cast < Base  *> ( new  Derived);
Base &b2 = d;
Base *pb3 = &d;
cout  <<  typeid(pb2).name() << endl//输出Base *
     << typeid(b2).name()<      << typeid(pb3).name()<      << typeid(*pb3).name()<
    因为Base不包含虚函数,所以typeid的结果指出,表达式的类型是Base或Base *型,尽管他们的底层对象是Derived。即: 当typeid操作符的操作数是不带有虚函数的类类型时,typeid操作符会指出操作数的类型,而不是底层对象的类型。
   
    下面在对Base函数做一个小小调整,为其加上一个虚函数,再看输出结果。
class  Base { virtual   void  f(){}; };
/*...*/
cout  <<  typeid(pb2).name() << endl//输出Base *
     <      <      < Derived
    这次Base含有虚函数,注意看结果,指针仍然是Base*的,尽管他们指向的是底层对象Derived,而这些Base对象的类型却是Derived的。
    因为指针pb3不是类类型,所以typeid就返回该指针pb3的指针类型Base *。而*pb3是一个类类型的表达式,而且该类带有虚函数,所以指出该pb3指向的底层对象的类型Derived。
    如果typeid操作符的操作数是至少包含一个虚拟函数的类类型时,并且该表达式是一个基类的引用,则typeid操作符指出底层对象的派生类类型。
    好了,文篇到此结束,留下几道小题目吧。
   
// 采用V8环境
cout <<  typeid( 7.84 << endl
    
<<  typeid(Base * << endl
    
<<  typeid( & pb3)  << endl;


13.输入输出流

转自:http://blog.csdn.net/zhangyifei216/article/details/50545572

为什么引入输入输出流?

因为C++兼容C,所以C中的输入输出函数依然可以在C++中使用,但是很显然如果直接把C的那套输入输出搬到C++中肯定无法满足C++的需求,第一点也是最重要的一点那就是C中的输入输出有类型要求,只支持基本类型,很显然这是没办法满足C++的需求的,因此C++设计了易于使用的并且多种输入输出流接口统一的IO类库。并且还支持多种格式化操作。还可以自定义格式化操作。C++中总体来说有三种输入输出流,第一种就是标准的输入输出,第二种是文件的输入输出,第三种是基于字符串的输入输出流。C++引入IO流,将这三种输入输出流接口统一起来,使用>>读取数据的时候,不用去官是从何处读取数据,使用<<写数据的时候也不需要管是写到哪里去。

标准输入输出流的

最常用的就是标准输出cout,标准输入cin,以及标准错误输出cerr,这三个其实就是istream,ostream这两个类的全局实例。标准的输入和输出流也是大多数情况下我们使用的最多的一种输入输出流了,前面说过,对于IO流来说可以支持自定义类型,通过给自定义类型重载标准输入和输出流可以让自定义类型支持IO流,通常这也是很方便实现的。下面给出一个具体的例子:

自定义类型Date的实现:

class Date
{
  public:
    Date(int year,int month,int day)
         :m_day(day),m_month(month),m_year(year)
    {
    }
    ~Date()
    {
    }
    int getMonth()const {
        return m_month;
    }
    int getDay()const {
        return m_day;
    }
    int getYear()const {
        return m_year;
    }

  private:
    int m_day;
    int m_month;
    int m_year;
};

上面是一个自定义类型,如何让其支持输入和输出流?,需要对输出流进行重载,函数原型如下:

ostream& operator<<(ostream& os,const Date& d);

很显然这个函数不能是成员函数,因为第一个参数必须是输出流类ostream,为了让输出流支持链式表达式,所以函数返回ostream的引用,如果要输入的数据是Date类的私有成员,可以将这个函数设置为Date类的友元函数,本文没有这样做,因为Date类已经将数据通过public接口输入了。下面是具体实现.

ostream& operator<<(ostream& os,const Date& d) {
    char fillc = os.fill('0');
    //设置填充字符,当输出长度小于指定的长度的时候用于进行填充
    os<2) << d.getMonth() << '-' //setw用来设置输出长度
      <2) << d.getDay() << '-' 
      <4) << setfill(fillc) << d.getYear();
      //setfill还原回原来的填充字符
    return os; 
}

上面使用了一些操作流的函数,下文会展开来讨论。到此为此为自定义类型添加对输出流的支持就完成了。接下来看看如何重载标准输入流吧,实现如下:

istream& operator>>(istream& is,Date& d) {
    is >> d.m_month;
    char dash;
    is >> dash;
    if (dash != '-')
        is.setstate(ios::failbit);
        //设置流错误状态,这使得下面的所有操作都会被忽略
    is >> d.m_day;
    is >> dash;
    if (dash != '-')
        is.setstate(ios::failbit);
    is >> d.m_year;
    return is;
}

输入流也很简单,同样它也不能是成员函数,但是在这输入流必须是友元函数,因为这里直接对Date类的私有成员进行了操作,所以需要在Date类的开始处添加友元声明,如下:

class Date
{
  public:
    friend istream& operator>>(istream& is,Date& d);
    .....

到此为此输入流重载也很简单的实现了,在输入流中用到了setstate这个函数,这是用来设置流的状态的,C++中给流设置了许多状态来,不同的状态效果不同的,在某些状态下将会导致输入输出流无效.这里通过
setstate将流设置为ios::failbit状态,这个状态将导致流不可用.因为这个输入流其实就是要求用户输入按照”-“分割的数字。例如下面是合法的:

08-10-2003
8-10-2003

因为m_month,m_day,m_year都是整型,所以如果你输入的不是整型那么同样也会导致流出现错误。导致流的状态发生改变。 下面就流的状态来谈谈对IO流操作的影响。

流的状态

C++中一共有四种流的状态,这些流状态标志位都是ios类的成员,如下:

badbit  发生了致命性错误,流无法继续使用
eofbit  输入结束,文件流物理上结束了,或者是用户按下ctrl+z或ctrl+d结束
failbit io操作失败,主要是操作了非法数据,流可以继续使用,输入结束后也将设置流为failbit
goodbit 一切正常,没有错误发生

C++同时也提供了一系列的函数用于测试这些状态位和操作状态位。

good() 判断流是否是goodbit状态
bad()  判断流是否是badbit状态
eof()  判断流是否是eofbit状态
fail() 判断流是否是badbitfailbit状态
clear() 清楚流的状态
这些都是io流类的成员函数,无论是标准输入输出流还是文件流还是字符串流,都是有这一系列的函数。

那么流的状态到底在什么情况下会发生改变呢,每一种状态会对io流操作产生什么影响呢?,这或许是我对流的状态这个知识点的疑问。下面来看一个例子。

int main()
{
    int a = -1;
    cin >> a;
    cout << "state:"<<cin.fail() << endl;
    cout << a << endl;
}

代码很简单,就是从标准输入接收一个数值,然后打印流的状态 ,下面是在不同的输入情况下的输出结果。

$ ./a.out 
1
state:0
1
$ ./a.out
q
state:1
0
$./a.out 
state:1
-1

第一次是输入了一个数字,正确被接收了,所以状态肯定不是failbit了,第二次输入是一个字符,所以cin会发生错误,流的状态会变成failbit,所以流的状态测试结果是true。但是一个意想不到的效果是a的值居然变成了0。当io流接收到一个错误的值的时候,io流会使用空值(0)来代替。第三次直接从键盘输入ctrl+d表示流结束,你会发现流的状态变成了failbit,正好对应上文。并且在这种情况下不会对接收变量做任何赋值操作。

处理流错误

在大部分情况下流是很少会出现错误的,但是为了程序的健壮性程序员可能需要使用测试流状态的函数去检测io流是否正常,因为io流出现再C++引入异常之前,所以错误处理方式仍是像C那样去检查错误码或者状态等来判断,因此C++一方面为了兼容早期的代码另一方面为了迎合异常错误处理,所以在io流错误处理这块可以通过抛出异常来进行错误处理。方法如下:

#include 
#include 
#include 
#include 
#include 

using namespace std;


/*
 *  流发生错误,需要手动查看其状态才能知道,这是早起c++不支持异常导致的,
 *  为了兼容,一致沿用,为了统一使用异常来处理错误,可以使用流的exception函数来设置当发生了指定流状态的时候,触发异常
 *
 */

int main()
{

    float a;
    cin.exceptions(ios::failbit);
    try {
        cin >> a; //接收了一个字符,触发了fail
    } catch(const std::ios_base::failure& fail) {
        cout <<"Get a error" << endl;    
        cout << fail.what() << endl;
    }
}

通过流的成员函数exceptions来设置当什么为某种状态的时候出发异常。到此为止标准输入输出流相关的内容基本结束了。

基于文件的的输入输出流

先看一段文件流使用的基本示列。

int main()
{

    ifstream in("in.txt");
    ostream out("out.txt");
    const int sz = 1024;
    char buf[SZ];
    while(in.getline(buf,SZ)) { //每读取一行,然后写到out.txt中
        out << cp << endl;
    }
}

所以对于文件输入输出流来说使用方式和标准输入输出流基本是一样的。就这么轻轻送送的操作文件了。getline是一个按行读取的函数,当读取到指定大小的数据还没有遇到换行符就返回。到遇到eof则getline返回false。如果对C语言中的文件输入输出熟悉的人来说,可能会发现少了打开模式和定位的操作。其实不然,C++也是有的。下面分别介绍下文件流的打开模式和流的定位操作

文件流的打开模式

 ios::in     打开输入文件,使的现存的文件不会被截断
 ios::out    打开输出文件,意味值是ios::trunc模式
 ios::app    打开文件,进行追加
 ios::ate    打开文件,指向文件末尾
 ios::trunc  打开文件,文件存在就截断旧文件
 ios::binary 按照二进制方式打开文件,默认打开为文本方式

可以通过ifstream和ostream声明实例的时候添加第二个参数,这些打开模式还可以通过|操作符来进行组合。这个部分内容很简单在这里就不使用例子演示了。

文件流的定位

流指针定位
ios::beg    流的开始位置
ios::cur    流的当前位置
ios::end    流的末端位置

通过流的seekg成员函数并传入定位的长度和定位的模式来进行IO流的定位操作。下面是一个使用流定位的例子:

#include 
#include 
#include 
#include 
using namespace std;

/*
 *  流指针定位
 *  ios::beg    流的开始位置
 *  ios::cur    流的当前位置
 *  ios::end    流的末端位置
 */

int main()
{
    const int STR_NUM = 5,STR_LEN = 30;
    char origData[STR_NUM][STR_LEN]  = {
        "Hickory dickory dus....",
        "Are you tired of C++?",
        "Well,if you have,",
        "That's just too bad",
        "There's plenty more for us!"    
    };

    char readData[STR_NUM][STR_LEN] = {{ 0 }};
    //二进制输入模式打开
    ofstream out("poem.in",ios::out|ios::binary);
    for(int i = 0; i < STR_NUM;i++)
        out.write(origData[i],STR_LEN);  //可以使用read,write折这样的接口
    out.close();                       //流是可以显示关闭的。

    ifstream in("poen.bin",ios::in|ios::binaray);
    in.read(readData[0],STR_LEN);
    assert(strcmp(readData[0],"Hickory dickory dbus...") == 0);

    in.seekg(-STR_LEN,ios::end);
    in.read(readData[1],STR_LEN);

    assert(strcmp(readData[1],"There's plenty more for us!") == 0);
    //默认是ios::beg定位模式
    in.seekg(3 * STR_LEN);
    assert(strcmp(readData[2],"That's just too bad") == 0);
    //从流的当前位置开始定位
    in.seekg(-STR_LEN * 2,ios::cur);
    in.read(readData[3],STR_LEN);
    assert(strcmp(readData[3],"Well, if you have,") == 0)
}

到此为此文件流相关的内容也基本介绍完毕了.

IO流缓冲

和fopen,fread系列函数一样,C++的输入输出流也是有缓冲机制的,为了避免每次去调用系统调用来获取磁盘的数据,输入输出流加入了缓冲机制,使用streambuf类进行了封装。streambuf内部维护了私有成员来保存缓冲数据streambuf类提供了一个rdbuf成员函数可以用来读取内部用来保存缓冲数据的私有成员。有了这个rdbuf成员函数就可以简单的完成很多事情,比如可以一次性读取整个文件的内容,而不用使用getline一行一行的读取了。
一个使用rdbuf的例子.

#include 
#include 
#include 

using namespace std;


int main()
{
    ifstream in("1.txt");
    ofstream out("1.out");

    out << in.rdbuf(); //copy file 文件一行拷贝
    in.close();
    out.close();

    ifstream in2("1.out",ios::in|ios::out);
    ostream out2(in2.rdbuf()); //out2中的内容是对in2的引用,并不是拷贝

    out2.seekp(0,ios::end); //默认流是在开始处,如果直接往里面写东西会导致覆盖开始处的字符,所以这里将流定位到结束处。
    out2 << "Where does this end up?";
    out2 << "And what about this?";
    in2.seekg(0,ios::beg);
    cout << in2.rdbuf(); //因为out2是对in2的一个引用,in2也会输出上面的两行

}

基于字符串的输入输出流

字符串流是我感觉和C语言差别最大的一个地方吧,在C++中可以使用输入输出操作符来操作字符串,基于字符串流可以实现像C语言使用scanf那样来分解输入参数,还可以格式化输出,字符串和数字之间的转换等。总之来说字符串流功能很强大,也是很多C++新手没有掌握的一把利器。先来看一段使用字符串流的代码来分割参数的例子。
利用字符串流实现参数分割,比fscanf好用多了。 同时也实现了字符串转数字的功能

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    istringstream s("47 1.414 This is a test"); //用户的输入
    int i;
    double f;
    s >> i >> f; //空白字符为分隔符输入,对输入进行了分割,提取了前面的整型和浮点型
    assert(i == 47);
    //演示了浮点型比较的正规做法
    double relerr = (fabs(f) - 1.414)  / 1.414;
    //double数据进行比较的时候不能使用恒等,应该使用这个epsilon
    //是两个double数的机器最小误差
    assert(relerr <= numeric_limits<double>::epsilon());
    string buf2;
    s >> buf2; //提取this
    assert(buf2 == "This");
    cout << s.rdbuf();  //输入
    cout << endl;
}

利用字符串输出流实现了fprintf的效果

#include 
#include 
#include 

using namespace std;

int main()
{
    cout << "type an in ,a float and a string: ";
    int i;
    float f;
    cin >> i >> f;
    string stuff;
    getline(cin,stuff);
    ostringstream os;
    os << "integer = " << i << endl;
    os << "float = " << f << endl;
    os << "string = " << stuff << endl;
    string result = os.str();
    cout << result << endl;
}

输出流的格式化

使用过printf的函数的人应该都知道printf可以根据传入格式化字符串来对输入进行格式化,那么C++的输入输出流也不例外同样也是可以对输出流进行格式化的,并且相比与printf来说更易于使用,下面会介绍一些常用的输出流格式化操作。

一些开关标志:

ios::skipws 跳过空格(输入流的默认情况,会跳过输入的空格)
ios::showbase 打印整型值时指出数字的基数,比如八进制输出的话就在前面加个0,十六进制输入就在前面加个0x
ios::showpoint 显示浮点值的小数点并阶段数字末尾的零
ios::uppercase 显示十六进制数值的时,使用大写A~F来代替
ios::showpos 显示整数前的加号
ios::unitbuf 单元缓冲区,每次插入后刷新流

上面这类是开关标志可以直接使用流的setf成员来设置,使用unsetf来取消。 下面给出一些使用的基本示列:

int main()
{
    int a = 14;
    cout.setf(ios::showbase);
    cout.setf(ios::oct,ios::basefield);//设置按照几进制输出
    cout << a << endl; //默认会按照十进制输出14,通过设置ios::oct,会按照八进制输出16,又因为开启了showbase,所以会输出016
}

ios::unitbuf是一个很值得探究的标志。如果这个标志没有开启,那么下面的代码在某些编译器上则可能只存入部分字符,

int main()
{

    ofstream out("log.txt");
    //当这个选项没有开启的时候,某些编译器仅写入一个字母'o'到文件log.txt
    //开启单元缓冲器后则不会丢失任何数据
    out.setf(ios::unitbuf);
    out << "one" << endl;
    out << "two" << endl;
    abort();
}

一些格式化操作:

ios::basefield//设置按照几进制输出 
ios::dec
ios::hex
ios::oct

ios::floatfield//浮点数显示相关
ios::scientific 按照科学计数显示浮点数
ios::fixed  按照固定格式显示浮点数, 

ios::adjustfield//数值对齐方式
ios::left   数值左对齐,使用填充字符右边填充
ios::right  数值右对齐 使用填充字符左边填充
ios::internal 数值中间对齐,左右填充

这类格式化操作通过setf成员函数来设置,不过需要在第二个参数处填入格式化操作所属于的类别例如cout.setf(ios::dec,ios::basefiled)还有一类格式化操作,用于设置宽带,填充字符,精度设置等。

ios::width()
ios::width(int n)
ios::fill()
ios::fill(int n)
ios::precision()
ios::precision(int n)

不带参数的用于返回当前的宽带,填充字符,和精度,带参数的用于设置宽度,填充字符和精度,返回的都是设置之前的值。这个部分使用起来还是很简单的,就不再举例子了。有的时候是不是感觉使用setf成员函数来设置这些标志位很麻烦,C++为了方便给我们提供了另外一种方式来设置输出格式化操作。如下:

比如cout.setf(ios::showbase)可以变成cout << showbase,看起来是不是清爽多了



迭代器

转自:http://blog.csdn.net/qq_35644234/article/details/52331948

迭代器的简介
(1):迭代器类似于指针类型,它也提供了对对象的间接访问。
(2):指针是c语言中就有的东西,迭代器是c++中才有的,指针用起来灵活高效,迭代器功能更丰富些。
(3):迭代器提供一个对容器对象或者string对象的访问的方法,并且定义了容器范围。

使用迭代器
迭代器和指针不一样,容器和string有迭代器类型同时拥有返回迭代器的成员。比如,容器都有的成员begin和end,其中begin成员复制返回指向第一个元素(第一个字符)的迭代器,而end成员返回指向容器(或string对象)尾元素的下一个位置的迭代器,也就是说end指示的是一个不存在的元素,所以叫end返回的是尾后迭代器。一般我们清楚一个迭代器的准确类型是什么,所以我们都是使用auto或者decltype来定义变量的。

vector<int> v;
auto b=v.begin();
decltype(v.begin()) b=v.begin();

标准容器迭代器的运算符

*iter      返回迭代器iter所指元素的引用
iter->men  解引用iter并获得该元素的名为men的成员,相当于(*iter).men
++iter      令iter指示容器的下一个元素
 --iter      令iter指示容器的上一个元素
iter1==iter2  如果两个迭代器指示的是同一个元素或者它指向同一个容器的尾后迭代器,则相等.

下面给出一个使用迭代器运算符修改容器元素的代码示例:

#include
using namespace std;
#include
int main()
{
    vector <int> v(10, 1);
    int i=1;
    cout << "未修改前:";
    for (auto v_quote : v)
    {
        cout << v_quote << " " ;   //未修改前
    }
    cout << endl;
    for (auto v_quote = v.begin(); v_quote != v.end(); ++v_quote)
    {
        *v_quote += i;
        ++i;
    }
    cout << "修改后:";
    for (auto v_quote : v)
    {
        cout << v_quote << " ";
    }
    cout << endl;
    system("pause");
    return 0;
}

输出结果:
这里写图片描述

迭代器的类型
那些拥有迭代器的标准库类型都是使用:iterator和const_iterator来表示迭代器的类型:

    vector <int> ::iterator it;        //it能读写vector的元素
    vector <int>::const_iterator it;  //it只能读vector的元素,不可以修改vector中的元素

    string::iterator s;               //s可以读写string对象中的元素
    string::const_iterator s;          //s只可以读string对象中的元素,不可以修改string对象中的元素

const_iterator和常量指针一样,只可以读取但不可以修改所指的值。在c++11的新标准中,为了便于得到const_iterator类型的返回值,引入两个新的函数,分别是cbegin和cend,功能类似于begin和end,只是返回的值类型为const_iterator;

vector动态增长的限制:
(1):不能再范围for循环中向vector对象添加元素。
(2):任何一种可能改变vector容量的操作,比如push_back,都会使该vector对象的迭代器失效。

迭代器的运算

iter+n           //迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置向前移动了,指示可能是容器的一个元素或者是尾部的下一个位置
iter-n          //相反,迭代器指示的位置向后移动了,指示可能是容器的一个元素或者是尾部的下一个位置
iter1+=n   //等价于iter1+n
iter1-=n    //等价于iter2-n
iter1-iter2  //两个迭代器的距离,
><,>=,<= //位置离begin近的较小

两个迭代器返回的值,在string和vector都为它定义了一个新的类型–difference_type,它是一个带符号的整型。

使用迭代器运算的一个经典算法是二分查找,代码如下:

#include
using namespace std;
#include
#include
int main()
{
    vector <int> v{ 1,20,10,5,6,7,90 };
    cout << "请输入需要查找的值:";
    int temp = 0;
    cin >> temp;
    sort(v.begin(), v.end());       //使用排序函数对容器vector中的元素进行排序

    auto beg = v.begin();
    auto end = v.end();
    auto mid = v.begin()+ (end - beg) / 2;
    /*
    二分查找法:
    每次都拿中间的那个值和需要查找的值做比较,如果相等就退出查找
    如果查找的值比中间那个值小,则向前查找
    如果查找的值比中间那个值大,则向后查找
    */
    while (mid != end && * mid != temp)
    {
        if (temp < *mid)
        {
            end = mid;
        }
        else
        {
            beg = mid+1;
        }

        mid = beg + (end - beg) / 2;

    }
    if (* mid == temp)
        cout << "find" << endl;
    else
        cout << "Not find" << endl;
    system("pause");
    return 0;
}

输出结果:
这里写图片描述






你可能感兴趣的:(c++大学教程以及相关内容笔记整理)