多态

(一)

先编写函数:

#include <iostream> 
using namespace std;
 
class Shape
{
   protected:
      int width, height;
      
   public:
      Shape( int a = 0, int b = 0)
      {
         width = a;
         height = b;
      }
      
      int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

class Rectangle: public Shape
{
   public:
      Rectangle( int a = 0, int b = 0)  
      { 
      }
      
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }
};

class Triangle: public Shape
{
   public:
      Triangle( int a=0, int b=0) : Shape(a, b) { }
      
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }
};


int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
 
   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();
 
   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();
   
   return 0;
}

运行结果:

Parent class area :
Parent class area :

程序分析:
(1)Shape( int a = 0, int b = 0)
这里直接给函数的形参赋了默认的初值,作用是:在函数调用时,省略部分或全部参数,这时就会使用默认参数进行代替
(2)Rectangle( int a = 0, int b = 0) : Shape(a, b) { }
这段代码的意思是,在构造函数Rectangle中,先调用父类的构造函数Shape。
(3)shape指针调用函数 area() 时,被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接--函数调用在程序执行前就准备好了。
有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

(二)

对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示

class Shape
{
   protected:
      int width, height;
      
   public:
      Shape( int a = 0, int b = 0)
      {
         width = a;
         height = b;
      }
      
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

运行结果:

Rectangle class area :
Triangle class area :

程序分析:
(1)虚函数
虚函数是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
(2)此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数。


引用

(一)C语言中的“&”

在C语言里,我们碰到过“&”这个符号。“&”的使用场景有两种:
(1)位运算符

int a = 5;
int b = 10;
int c = a & b;

(2)取地址符

int a;
scanf("%d", &a);

(二)C++语言中的“&”

在C++里,“&”的使用场景有三种:
(1)位运算符,这在C, C++, Java等语言中,都是一样的

(2)取地址符,这是因为C++兼容了C

#include <iostream>
using namespace std;

int main() 
{
    int a;
    printf("Input a = ");
    scanf("%d", &a);
    printf("a = %d", a);
    
    return 0;
}

(3)作为引用
这是C++中加入的新语言特性。
所谓引用,就是变量的别名。
比如有一个人叫做Teodoro,他有一个外号叫做“小朋友”,那么“小朋友”就是Teodoro的引用。别人说“小朋友”或Teodoro,说的都是同一个人。

引用的语法为:类型 &引用名 = 变量名
比如

int a;
int &ra = a; 

操作引用,就是操作变量本身。
例子:

#include <iostream>
using namespace std;

int main() 
{
    int a; 
    int &ra = a;
    ra = 1;
    
    printf("Memory address of ra: %d\n", &ra);
    printf("Memory address of a: %d\n", &a);
    printf("ra = %d\n", ra);
    printf("a = %d\n", a);
    
    return 0;
}

运行结果:

Memory address of ra: 2293316
Memory address of a: 2293316
ra = 1 
a = 1

可见,ra和a的内存地址是一样的,值自然也一样。ra和a实际上是同一回事。

两数交换

之前学C语言的时候,咱们直接在main函数中使用“异或”位运算符,很容易实现了两数交换。
本节课将在此基础上,把交换两个数的算法,封装到swap函数中。这样不管是哪个地方想要交换两个数,调用swap函数就可以了。

程序1:

#include <iostream>
using namespace std;

void swap(int m, int n)
{
    cout << "Memory address of m: " << &m << endl;
    cout << "Memory address of n: " << &n << endl;
    
    cout << "\nBefore Swap:" << endl;
    cout << "m = " << m << endl;
    cout << "n = " << n << endl;
    
    m ^= n;
    n ^= m;
    m ^= n;
    
    cout << "\nAfter Swap:" << endl;
    cout << "m = " << m << endl;
    cout << "n = " << n << endl;    
}

int main(int argc, char** argv) 
{
    int a = 1;
    int b = 2;

    cout << "Memory address of a: " << &a << endl;
    cout << "Memory address of b: " << &b << endl;  
    
    swap(a, b);
    
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    
    return 0;
}

运行结果:

Memory address of a: 0x7fff90371cd8
Memory address of b: 0x7fff90371cdc
Memory address of m: 0x7fff90371c9c
Memory address of n: 0x7fff90371c98

Before Swap:
m = 1
n = 2

After Swap:
m = 2
n = 1
a = 1
b = 2

分析:
从结果可以看出,a和b调用swap之后,值并没对换过来。
其原因在于,形参m和n的作用域只是在swap函数内。在swap内,m = a = 1, n = b = 2,交换后m = 2, n = 1。但是,m和n的值并不会传回给a和b,导致a和b的值没有被对换。

解决方案,使用上节课讲过的引用。
程序2:

#include <iostream>
using namespace std;

void swap(int &m, int &n)
{
    cout << "Memory address of m: " << &m << endl;
    cout << "Memory address of n: " << &n << endl;
    
    cout << "\nBefore Swap:" << endl;
    cout << "m = " << m << endl;
    cout << "n = " << n << endl;
    
    m ^= n;
    n ^= m;
    m ^= n;
    
    cout << "\nAfter Swap:" << endl;
    cout << "m = " << m << endl;
    cout << "n = " << n << endl;    
}

int main(int argc, char** argv) 
{
    int a = 1;
    int b = 2;

    cout << "Memory address of a: " << &a << endl;
    cout << "Memory address of b: " << &b << endl;  
    
    swap(a, b);
    
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    
    return 0;
}

运行结果:

Memory address of a: 0x7fffd009de98
Memory address of b: 0x7fffd009de9c
Memory address of m: 0x7fffd009de98
Memory address of n: 0x7fffd009de9c

Before Swap:
m = 1
n = 2

After Swap:
m = 2
n = 1
a = 2
b = 1

分析:
使用引用后,达到了交换a和b的目的。这是因为形参m和n是实参a和b的引用,m和a是一回事,n和b是一回事。
交换m和n的值,就是交换a和b的值。


多继承

单继承:子类(派生类)只能有一个父类(基类)。支持单继承的语言有Java, Objective-C, PHP, C#等。

多继承:子类(派生类)可以有多个父类(基类)。支持多继承的语言有C++, Python等。

程序1:

#include <iostream>
using namespace std;

class A
{
public:
    // 构造函数
    A()
    {
        cout << "A's constructor method is invoked!" << endl; 
    } 
};

class B
{
public:
    // 构造函数
    B()
    {
        cout << "B's constructor method is invoked!" << endl; 
    } 
};

// C继承自A和B 
class C : public A, public B
{
public:
    // 构造函数 
    C()
    {
        cout << "C's constructor method is invoked!" << endl;       
    }
};

int main(int argc, char** argv) 
{
    C c;
    return 0;
}

运行结果:

A's constructor method is invoked!
B's constructor method is invoked!
C's constructor method is invoked!



举现实中的一个例子:农民工,既是农民,又是工人。所以农民工继承自农民和工人。

程序2:

#include <iostream>
using namespace std;

class Farmer
{
public:
    Farmer()
    {
        cout << "I am a farmer" << endl;    
    }   
};

class Worker
{
public:
    Worker()
    {
        cout << "I am a worker" << endl;
    }

};

class MigrantWorker : public Farmer, public Worker
{
public:
    MigrantWorker()
    {
        cout << "I am a migrant worker" << endl;
    }
};

int main(int argc, char** argv) 
{
    MigrantWorker m;
    
    return 0;
}

运行结果:

I am a farmer
I am a worker
I am a migrant worker

C++创建对象的3种方式

先看程序:

#include <iostream>
using namespace std;

class A
{
private:
    int n;
public:
    A(int m)
    {
        n = m;
        cout << "Constructor method is invoked!" << endl;   
    }
    
    void printNum()
    {
        cout << "n = " << n << endl;
    }
};

int main()
{
    // 第一种 
    A a1(1);            // a1在栈中 
    a1.printNum();
    
    // 第二种 
    A a2 = A(2);        // a2在栈中 
    a2.printNum();
    
    // 第三种 
    A *a3 = new A(3);   // a3所指的对象在堆中,但是a3本身放在栈中 
    a3->printNum();
    delete a3;
        
    return 0;
}

运行结果:

Constructor method is invoked!
n = 1
Constructor method is invoked!
n = 2
Constructor method is invoked!
n = 3

分析:
(1)第一种方法和第二种方法写法略有差异,但本质上是一样的。

(2)一个由C/C++编译的程序占用的内存分为以下四个部分:
① 栈区(stack)--由编译器自动分配释放,存放函数的参数值,局部变量的值等。
② 堆区(heap)--由程序员分配释放。若程序员不释放,程序结束时可能由OS回收。 堆区的大小要远远大于栈区。
③ 全局区(静态区)(static)--全局变量和静态变量的存储是放在一块的。
里面细分有一个常量区,字符串常量和其他常量也存放在此。
该区域是在程序结束后由操作系统释放。
④ 程序代码区--存放函数体的二进制代码。 也是由操作系统进行管理的。

(3)a1和a2,都是局部变量,放在栈里。
指针a3本身放在栈区,但是它指向的对象,即new A(),放在堆里。
用malloc或new出来的对象,都是放在堆里。
cout << a3,这样得到的地址是指针a3所指的对象的地址,在堆里。
cout << &a3,这样得到的地址,是指针a3本身的地址,在栈里。
(4)new出来的对象,使用完之后,要用delete语句来释放内存。

析构函数

析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

析构函数名也应与类名相同,只是在函数名前面加一个位取反符,例如A( )。以区别于构造函数。
与构造函数一样,析构函数不能有返回值。不同的是,析构函数,也不能带参数,并且一个类只能有一个析构函数。

如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数。
许多简单的类中没有使用用显式的析构函数。

例子:

#include <iostream>
using namespace std;

class A
{
public:
    A()
    {
        cout << "Constructor method is invoked!" << endl;   
    }
    
    ~A()
    {
        cout << "Deconstructor method is invoked!" << endl;
    }
    
    void sayHi()
    {
        cout << "Hi!" << endl;
    }
};

int main()
{   
    A *a = new A();
    a->sayHi();
    delete a;
        
    return 0;
}

运行结果:

Constructor method is invoked!
Hi!
Deconstructor method is invoked!

标准库vector类

vector(向量)是 C++中的一种数据结构,也是一个类。它相当于一个动态的数组,当程序员无法知道自己需要的数组的规模多大时,用其来解决问题可以达到最大节约空间的目的。

一、定义和初始化

vector<T> v1; // T为类型名,比如int, float, string等
vector<T> v2(v1); // 将v1赋值给v2
vector<T> v3(n,i); // v3中含n个值为i的元素

二、操作

v.empty(); // 判断v是否为空
v.size(); // 返回v的长度
v.begin(); // 返回v的第一个元素
v.end(); // 返回v最后一个元素的下一个元素的值(指向一个不存在的值)
v.push_back(a); //在v的最后添加元素a(a必须与v的类型一致)

三、迭代器(iterator)

迭代器相当于指针,用来储存vector中元素的位置,即下标

定义:

vector<T>::iteartor iter = v.begin();   //iter指向v第一个元素
vector<T>::iteartor iter = v.end();     //iter指向v最后一个元素的下一个元素,这个元素不存在,通常用作循环结束的标志
*iter                                           //*iter指向的元素的值,可对其进行赋值
iter++                                      //iter指向v下一个元素

常见用法:
for(vector<T>::iterator iter = v.begin(); iter != v.end(); ++iter)
{
}

四、例子

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

int main(int argc, char** argv) 
{
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    
    cout << "Size of v is: " << v.size() << endl;
    for(vector<int>::iterator iter = v.begin(); iter != v.end(); iter++)
    {
        cout << *iter << ' ';   
    }
    cout << endl << endl;
    
    vector<float> v2(5, 3.33);
    cout << "Size of v2 is: " << v2.size() << endl;
    for(vector<float>::iterator iter = v2.begin(); iter != v2.end(); iter++)
    {
        cout << *iter << ' ';   
    }
    
    return 0;
}

运行结果:

Size of v is 3
1 2 3

Size of v2 is 5
3.33 3.33 3.33 3.33 3.33

 

函数模板

 

先看一段微软实现math.h中求幂的源码

template<class _Ty> inline
        _Ty _Pow_int(_Ty _X, int _Y)
        {unsigned int _N;
        if (_Y >= 0)
                _N = _Y;
        else
                _N = -_Y;
        for (_Ty _Z = _Ty(1); ; _X *= _X)
                {if ((_N & 1) != 0)
                        _Z *= _X;
                if ((_N >>= 1) == 0)
                        return (_Y < 0 ? _Ty(1) / _Z : _Z); }}

这里template表示模板。

在了解模板之前,咱们先来求一下两个 int型的和,两个float型的和,两个double型的和

#include <iostream>
#include <iomanip>

using namespace std;

int sum(int x, int y)
{
    return x + y;
}

float sum(float x, float y)
{
    return x + y;
}

double sum(double x, double y)
{
    return x + y;
}

int main()
{
    int a = 1, b = 2;
    // 调用int sum(int, int)
    cout << sum(a, b) << endl;

    float c = 1.1, d = 2.2;
    // 调用float sum(float, float)
    cout << sum(c, d) << endl;

    double e = 1.1111111111, f = 2.2222222222;
    // 调用double sum(double, double)
    cout << fixed << setprecision(10) << sum(e, f) << endl;

    return 0;
}

运行结果:

3
3.3
3.3333333333

分析:这里定义了三种类型的sum函数,假如只定义了int型的sum函数,那么编译的时候碰到sum(c, d)和sum(e, f)会报错。因为编译器找不到float和double的sum函数。

上面三个sum函数,除了函数返回类型和参数类型不一样外,功能是一样的,那么有没有办法使用一个通用的函数,就能同时满足int型、float型、double型的求和功能呢?
答案是有的。这就要用到函数模板。

#include <iostream>
#include <iomanip>

using namespace std;

template<class T>
T sum(T x, T y)
{
    return x + y;
}

int main()
{
    int a = 1, b = 2;
    // 调用int sum(int, int)
    cout << sum(a, b) << endl;

    float c = 1.1, d = 2.2;
    // 调用float sum(float, float)
    cout << sum(c, d) << endl;

    double e = 1.1111111111, f = 2.2222222222;
    // 调用double sum(double, double)
    cout << fixed << setprecision(10) << sum(e, f) << endl;

    return 0;
}

运行结果:

3
3.3
3.3333333333

分析:这个程序的关键是template<class T>,表示用了函数模板。class是关键字,代表着某种类型,这种类型在编译的时候,会根据被调用的参数而自动匹配。碰到int就是int型,碰到float就是float型,碰到double就是double型。

除了class关键字,还可以使用typename关键字,效果完全一样:

#include <iostream>
#include <iomanip>

using namespace std;

template<typename T>
T sum(T x, T y)
{
    return x + y;
}

int main()
{
    int a = 1, b = 2;
    // 调用int sum(int, int)
    cout << sum(a, b) << endl;

    float c = 1.1, d = 2.2;
    // 调用float sum(float, float)
    cout << sum(c, d) << endl;

    double e = 1.1111111111, f = 2.2222222222;
    // 调用double sum(double, double)
    cout << fixed << setprecision(10) << sum(e, f) << endl;

    return 0;
}

最后,咱们测试一下开头展示的快速幂函数:

#include <iostream>
#include <iomanip>

using namespace std;

template<class _Ty>
_Ty _Pow_int(_Ty _X, int _Y)
{
    unsigned int _N;
    if (_Y >= 0)
    {
        _N = _Y;
    }
    else
    {
        _N = -_Y;
    }

    for (_Ty _Z = _Ty(1); ; _X *= _X)
    {
        if ((_N & 1) != 0)
        {
            _Z *= _X;
        }

        if ((_N >>= 1) == 0)
        {
            return (_Y < 0 ? _Ty(1) / _Z : _Z);
        }
    }
}

int main()
{
    float a = 10;
    cout << _Pow_int(a, -2) << endl;

    int b = 15;
    cout << _Pow_int(b, 2) << endl;

    double c = 1.11111111;
    cout << fixed << setprecision(8) << _Pow_int(c, 8) << endl;

    return 0;
}

运行结果:

0.01
225
2.32305729

内联函数

一、何谓内联函数

上一节课中,我们分析了这一段函数:

template<class _Ty> inline
        _Ty _Pow_int(_Ty _X, int _Y)
        {unsigned int _N;
        if (_Y >= 0)
                _N = _Y;
        else
                _N = -_Y;
        for (_Ty _Z = _Ty(1); ; _X *= _X)
                {if ((_N & 1) != 0)
                        _Z *= _X;
                if ((_N >>= 1) == 0)
                        return (_Y < 0 ? _Ty(1) / _Z : _Z); }}

这里用到了关键字inline。
inline表示被修饰的函数内联函数。

二、为何使用内联函数

比如有一个要求两个整数的最大值,可以有三种写法:
(1)

a > b ? a : b 

(2)

int max(int a, int b)
{
    return a > b ? a : b;
}

(3)

inline int max(int a, int b)
{
    return a > b ? a : b;
}

第(2)种方法比第(1)种方法,有三个优点:
① 阅读和理解函数 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多
② 如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多
③ 函数可以重用,不必为其他应用程序重写代码
但也有一个缺点:
调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行

C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开,假设我们将 max 定义为内联函数,即上面第(3)种方式,那么若调用的代码为

cout << max(a, b) << endl;

则编译时,会自动展开为

cout << (a > b ? a : b) << endl;

从而消除了把 max写成函数的额外执行开销。

三、内联函数与宏的比较

宏本身没有安全检查,纯粹是简单替换,会引起很多语义错误,所以C++倒是提倡用const和内联代替宏。

四、内联函数必须放在函数定义的前面

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

如下风格的函数 Foo 不能成为内联函数:

inline void Foo(int x, int y);   // inline 仅与函数声明放在一起   
void Foo(int x, int y)
{
 ...
} 

而如下风格的函数 Foo 则成为内联函数:

void Foo(int x, int y);   
inline void Foo(int x, int y)   // inline 与函数定义体放在一起
{
 ...
} 

而如下风格的函数 Foo 也能成为内联函数:

inline void Foo(int x, int y);   
inline void Foo(int x, int y)   // inline 与函数定义体放在一起
{
 ...
} 

所以说,C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
一般地,用户可以阅读函数的声明,但是看不到函数的定义。

五、慎用内联函数

只有当函数只有 10 行甚至更少时才将其定义为内联函数。

当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论: 一个较为合理的经验准则是, 不要内联超过 10 行的函数.
谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行)。
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数)。


命名空间

(一)

先看一个简单的程序:

#include <iostream>
using namespace std;

int main()
{
    int a = 1;
    cout << a << endl;
}

运行结果:

1

这里的第一行,#include<iostream>好理解,iostream是输入输出流,包含了输入流istream和输出流ostream。
第二行using namespace std;
这里的namespace是个关键字,表示它后面的std是一个命名空间。

什么是命名空间呢?
假定三年1班有一名小学生叫做寒上耕,三年2班也有一名小学生叫做寒上耕,四年3班也有一名小学生叫寒上耕,假如有个人说:我要找寒上耕。那么没人知道他要找哪位寒上耕。但是假如有个人说:我要找三年1班的寒上耕,大家就知道他要找的是认谁。
这里的三年1班,三年2班,四年3班,就是命名空间,作用是防止命名冲突。

那么程序里为何要使用命名空间std呢?不用行不行?
若把using namespace std;这行代码去掉,cout和endl会报错,程序不认识这两个词。

(二)

咱们看一个iostream中的代码(我用的编译器是CodeBlocks):

#ifndef _GLIBCXX_IOSTREAM
#define _GLIBCXX_IOSTREAM 1

#pragma GCC system_header

#include <bits/c++config.h>
#include <ostream>
#include <istream>

namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION

  /**
   *  @name Standard Stream Objects
   *
   *  The &lt;iostream&gt; header declares the eight <em>standard stream
   *  objects</em>.  For other declarations, see
   *  http://gcc.gnu.org/onlinedocs/libstdc++/manual/io.html
   *  and the @link iosfwd I/O forward declarations @endlink
   *
   *  They are required by default to cooperate with the global C
   *  library's @c FILE streams, and to be available during program
   *  startup and termination. For more information, see the section of the
   *  manual linked to above.
  */
  //@{
  extern istream cin;       /// Linked to standard input
  extern ostream cout;      /// Linked to standard output
  extern ostream cerr;      /// Linked to standard error (unbuffered)
  extern ostream clog;      /// Linked to standard error (buffered)

#ifdef _GLIBCXX_USE_WCHAR_T
  extern wistream wcin;     /// Linked to standard input
  extern wostream wcout;    /// Linked to standard output
  extern wostream wcerr;    /// Linked to standard error (unbuffered)
  extern wostream wclog;    /// Linked to standard error (buffered)
#endif
  //@}

  // For construction of filebuffers for cout, cin, cerr, clog et. al.
  static ios_base::Init __ioinit;

_GLIBCXX_END_NAMESPACE_VERSION
} // namespace

#endif /* _GLIBCXX_IOSTREAM */

咱们看到了在iosteam中,cin(输入),cout(输出),cerr(错误),clog(日志)都是在std里定义的。

若你用的是Mac系统的Xcode编译器,则iostream头文件中的内容如下所示:

C++ -*-
//===--------------------------- iostream ---------------------------------===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is dual licensed under the MIT and the University of Illinois Open
// Source Licenses. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//

#ifndef _LIBCPP_IOSTREAM
#define _LIBCPP_IOSTREAM

/*
    iostream synopsis

#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

namespace std {

extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;

}  // std

*/

#include <__config>
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

_LIBCPP_BEGIN_NAMESPACE_STD

#ifndef _LIBCPP_HAS_NO_STDIN
extern _LIBCPP_FUNC_VIS istream cin;
extern _LIBCPP_FUNC_VIS wistream wcin;
#endif
#ifndef _LIBCPP_HAS_NO_STDOUT
extern _LIBCPP_FUNC_VIS ostream cout;
extern _LIBCPP_FUNC_VIS wostream wcout;
#endif
extern _LIBCPP_FUNC_VIS ostream cerr;
extern _LIBCPP_FUNC_VIS wostream wcerr;
extern _LIBCPP_FUNC_VIS ostream clog;
extern _LIBCPP_FUNC_VIS wostream wclog;

_LIBCPP_END_NAMESPACE_STD

#endif  // _LIBCPP_IOSTREAM

可以看到,CodeBlocks和Xcode关于iostream的内容,有不小的差异。但是一些关于C++的标准,还是一致的。

(三)

那么endl是在哪里定义的呢?
咱们看一下定义endl的源码,具体是定义在ostream里面

#ifndef _GLIBCXX_OSTREAM
#define _GLIBCXX_OSTREAM 1

#pragma GCC system_header

#include <ios>
#include <bits/ostream_insert.h>

namespace std _GLIBCXX_VISIBILITY(default)
{
  ......
  ......
_GLIBCXX_BEGIN_NAMESPACE_VERSION
  template<typename _CharT, typename _Traits>
    inline basic_ostream<_CharT, _Traits>&
    endl(basic_ostream<_CharT, _Traits>& __os)
    { return flush(__os.put(__os.widen('\n'))); }
  ......
  ......
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace std

#include <bits/ostream.tcc>

#endif  /* _GLIBCXX_OSTREAM */

可以看到,endl是定义在ostream中的std中。
因为iostream头文件已经包含了ostream头文件和istream头文件,所以咱们的代码中只需要包含iostream头文件即可。

另一方面,咱们注意到,在iostream头文件和ostream头文件中都包含了命名空间std。可见命名空间可以散布到不同的头文件。事实上,std不止在这两个头文件中有,在其他的头文件中也有,作用是把一些常用的操作符都包含进来。然后在你写的代码中引入using namespace std;后,即可减少命名冲突。

(四)

假如咱们不写using namepace std;那么在使用相关的操作符时,需要加上std

#include <iostream>

int main()
{
    int a = 1;
    std::cout << a << std::endl;
}

再看一个例子

#include <iostream>

namespace A
{
    int x = 10;
};

namespace B
{
    int x = 20;
};

using namespace B;

int main()
{
    std::cout << x << std::endl;
}

这个程序里,有两个命名空间A和B,都定义了变量x。因为已经说明了using namespace B; 所以打印出20。

(五)

若没说明使用哪个命名空间,必须在x前面指明命名空间,否则编译会报错。

#include <iostream>

namespace A
{
    int x = 10;
};

namespace B
{
    int x = 20;
};

int main()
{
    std::cout << B::x << std::endl;
    std::cout << A::x << std::endl;
}

运行结果:

20
10

cin与scanf,cout与printf的效率比较

一、生成测试数据

先把1000万个测试数据写入data.txt中

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

const int num=10000000;

int main()
{
    ofstream fout("data.txt");
    clock_t t1, t2;
    t1 = clock();
    for(int i = 0; i < num; i++)
    {
        fout << i << ' ';
    }
    t2 = clock();
    cout << "Running time: " << t2 - t1 << " ms" << endl;
    return 0;
}

运行结果:

4764 ms

电脑的配置为i7处理器,16G内存,运行时间接近5秒,生成的data.txt文件大小为77040k,即77M左右。

二、cin与scanf的效率比较

(一)

#include<iostream>
#include<ctime>
#include<cstdio>
using namespace std;
const int num=10000000;
int main()
{
    freopen("data.txt", "r", stdin);

    int i, x;
    clock_t t1, t2;
    t1 = clock();
    for(i = 0; i < num;i++)
    {
        cin >> x;
    }
    t2 = clock();
    cout << "Runtime of cin: " << t2 - t1 << " ms" << endl;

    clock_t t3, t4;
    t3 = clock();
    for(i = 0; i < num;i++)
    {
        scanf("%d", &x);
    }
    t4 = clock();
    cout << "Runtime of scanf: " << t4 - t3 << " ms" << endl;

    return 0;
}

运行结果:

Runtime of cin: 16363 ms
Runtime of scanf: 13753 ms

分析:
1 先要把测试数据文件data.txt拷贝到当前工程目录下
2 stdin是C语言的标准输入流,表示先把data.txt中的数据读取到标准输入流里面,然后用cin >> x的时候,就不会要求用户从控制台输入数据,而是从stdin中读取数据
3 从运行结果可以看出,输入1千万个数据,scanf的效率只比cin快一点。这与通常说的scanf效率远高于cin不符。(互联网随便一搜索都是scanf效率比cin高)
这是因为,这里我使用的集成开发环境(IDE)是CodeBlocks,内置了G++,G++是C++的一种编译器,G++对cin和cout做了优化,会大幅提高cin和cout的效率。

 
1.png

(二)对cin进行加速

下面两行代码可以提升cin和cout的效率

    ios::sync_with_stdio(true);
    cin.tie(0);

完整代码为:

#include<iostream>
#include<ctime>
#include<cstdio>
using namespace std;
const int num=10000000;

int main()
{
    freopen("data.txt", "r", stdin);

    ios::sync_with_stdio(false);
    cin.tie(0);

    int i, x;
    clock_t t1, t2;
    t1 = clock();
    for(i = 0; i < num;i++)
    {
        cin >> x;
    }
    t2 = clock();
    cout << "Runtime of cin: " << t2 - t1 << " ms" << endl;

    clock_t t3, t4;
    t3 = clock();
    for(i = 0; i < num;i++)
    {
        scanf("%d", &x);
    }
    t4 = clock();
    cout << "Runtime of scanf: " << t4 - t3 << " ms" << endl;

    return 0;
}

运行结果:

Runtime of cin: 4925 ms
Runtime of scanf: 13777 ms

可以看到,加了两句代码后,cin的效率有了大幅提高,从16秒缩短为5秒!

三、sync_with_stdio与cin.tie分析

(一)ios::sync_with_stdio()

sync_with_stdion的参数默认值为true,表示cin与scanf同步,cout与printf同步。

同步(sync)是什么意思呢?
比如有几个人排队去打水,水龙头只有一个。那么只有前面的人打完水,后面的人才能打水,这种情况下,必然是排在前面的人比后面的人先打完水。
与同步对应的是异步(async),异步又是什么回事呢?
比如有几个人排队去打水,水龙头有好多个。那么排在前面的人未必会比排在后面的人先打完水。比如排在第1位的人选了1号水龙头,排在第2位的人紧接着选了2号水龙头,假如2号水龙头出水的速度远大于1号水龙头,那么排在第2位的人,会比排在第1位的人先打完水。

同步和异步只有在cin和scanf(或cout与printf)混用的情况下才有意义。默认情况下执行的是ios::sync_with_stdio(true),表示cin和scanf是按被程序调用的顺序先后执行的。

cin >> a;
scanf("%d", &b);
cin >> c;
scanf("%d", &d);

这里的输入顺序一定是a, b, c, d。
若改为ios::sync_with_stdio(false),则输入顺序不一定是a,b,c,d。
当使用了ios::sync_with_stdio(false),cin和scanf,cout和printf就不要再混用了。因为异步可能会导致意想不到的后果。

(二)cin.tie(0)

这里的tie表示绑定,在默认的情况下cin绑定的是cout,每次执行 << 操作符的时候都要调用flush(即清空缓存),这样会增加IO(输入输出)负担。可以通过tie(0)(0表示NULL)来解除cin与cout的绑定,进一步加快执行效率。

默认情况下cout << "123" << endl; 也可以写成cin.tie() << "123" << endl;
cin.tie(0)等价于cin.tie(NULL),表示不与任何输出流相绑定,即解绑了默认的cout。
若此时再使用cin.tie() << "123" << endl; 编译器会报错。

可通过下面的程序来加深理解:

#include <iostream>
#include <fstream>
#include <windows.h>

using namespace std;

int main()
{
    ostream *prevstr;

    ofstream ofs;                           // 文件输出流
    ofs.open("test.out");

    cout << "Example of tie method\n";      //直接输出至控制台窗口

    *cin.tie() << "Insert into cout\n";     // 空参数调用返回默认的output stream,也就是cout

    prevstr = cin.tie(&ofs);                // cin绑定新的输出流指针ofs,并返回上一次绑定的输流指针即cout
    *cin.tie() << "Insert into file\n";     // ofs,输出到文件test.out中

    cin.tie(prevstr);                       // 恢复原来的output stream,即cout
    *cin.tie() << "Insert into cout again\n";

    ofs.close();                            //关闭文件输出流

    return 0;
}

运行结果:
控制台上打印出

Example of tie method
Insert into cout
Insert into cout again

还生成了test.out文件,内容为

Insert into file

四、cout与printf的效率比较

(一)在控制台输出10万个数据进行测试

#include<iostream>
#include<cstdio>
#include<ctime>

using namespace std;
const int num=100000;

int main()
{
    int i;
    clock_t t5, t6;
    t5 = clock();
    for(i = 0; i < num;i++)
    {
        cout << i << ' ';

    }
    t6 = clock();

    clock_t t7, t8;
    t7 = clock();
    for(i = 0; i < num;i++)
    {
        printf("%d ", i);
    }
    t8 = clock();

    cout << endl << "Runtime of cout: " << t6 - t5 << " ms" << endl;
    cout << "Runtime of printf: " << t8 - t7 << " ms" << endl;

    return 0;
}

运行结果:

Runtime of cout: 10852 ms
Runtime of printf: 19753 ms

可以看出,cout运行效率比printf要高,这是G++编译器对cin和cout优化的结果。

(二)对cout进行加速

#include<iostream>
#include<cstdio>
#include<ctime>

using namespace std;
const int num=100000;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    int i;
    clock_t t5, t6;
    t5 = clock();
    for(i = 0; i < num;i++)
    {
        cout << i << ' ';

    }
    t6 = clock();

    clock_t t7, t8;
    t7 = clock();
    for(i = 0; i < num;i++)
    {
        printf("%d ", i);
    }
    t8 = clock();

    cout << endl << "Runtime of cout: " << t6 - t5 << " ms" << endl;
    cout << "Runtime of printf: " << t8 - t7 << " ms" << endl;

    return 0;
}

运行结果:

Runtime of cout: 546 ms
Runtime of printf: 19597 ms

可见加速后,cout的运行速度更快了。

五、结论

(1)scanf/printf需要格式化符号%d, %f, %c之类的,这是不如cin/cout方便的地方
(2)cout在控制小数位输出时,很不方便。需要如此操作

#include<iomanip>
cout << fixed << setprecision(5) << endl;   // 输出5位小数

(3)对于某些优化过cin和cout的编译器(比如G++)而言,cin/cout的运行效率比scanf/printf高。
但是对于没做过优化的编译器,则是scanf/printf的效率大大高于cin/cout。互联网上能搜到的文章,几乎都是这种情况。这与本篇的实验结果恰好相反。
(4)对于非算法比赛而言,用cin/cout或scanf/printf无所谓。
(5)但是对于算法比赛而言,因为数据量大,经常会导致超时(TLE--Time limit exceeded)。此时可统一使用scanf/printf,或者使用加了ios::sync_with_stdio(false); cin.tie(0)的cin/cout。

 


C与C++的文件输入输出

一、fopen

fopen是C语言中的标准输入输出函数,被包含在<stdio.h>中。
C语言中,用fopen来打开文件。
函数声明:FILE * fopen(const char * path, const char * mode);
path表示文件的路径,可用当前路径或绝对路径,若文件名之前没加路径,表示当前工程所在的路径。
mode表示使用文件方式,是文件的类型和操作要求,有如下几种:

r(read):只读;
w(write):只写;
a(append):追加;
t(text):文本文件,可省略;
b(binary):二进制文件。
+ :读和写

例1

#include <stdio.h>

int main()
{
    FILE *fp1, *fp2; //定义文件指针类型
    // input.in文件中放了两个整数
    fp1 = fopen("input.in","r"); //用fopen函数以只读方式(r)打开输入文件input.in;
    fp2 = fopen("output.out","w");//用fopen函数以写入方式(w)打开输出文件output.out;

    int a, b;
    fscanf(fp1, "%d %d", &a, &b);//fscanf从文件中读取数据,fp1文件指针指定文件;
    fprintf(fp2, "%d", a + b);//fprintf将数据输出到文件,fp2文件指针指定文件;

    fclose(fp1);//关闭文件指针。
    fclose(fp2);

    return 0;
}

在启用fopen函数时,需要特定的函数fscanf,fprintf对文件操作;普通的scanf和printf依然能在命令行界面操作。
最后要用fclose关闭文件。

二、freopen

freopen中的re代表redirect,重定向。
freopen()函数重定向了标准流,使其指向指定文件,因此不需要修改scanf和printf。
所谓重定向输出,就是可以把原本只是输出在控制台的字符,输出到你指定的路径文件中。(输入类似,就是从指定的文件中读取,而不是读取在控制台中的输入。)
重定向函数可以在任何时候开启、关闭。

函数名:freopen
标准声明:FILE *freopen( const char *path, const char *mode, FILE *stream );
所在文件: <stdio.h>
参数说明:
path: 文件名,用于存储输入输出的自定义文件名。
mode: 文件打开的模式。和fopen中的模式(如r-只读, w-写)相同。
stream: 一个文件,通常使用标准流文件。
返回值:成功,则返回一个path所指定文件的指针;失败,返回NULL。
功能:实现重定向,把预定义的标准流文件定向到由path指定的文件中。标准流文件具体是指stdin、stdout和stderr。其中stdin是标准输入流,默认为键盘;stdout是标准输出流,默认为屏幕;stderr是标准错误流,一般把屏幕设为默认。

例2

#include <stdio.h>

int main()
{
    int a,b;
    // in.txt中有三行数:
    // 10 20
    // 11 22
    // 12 24
    freopen("D:\\in.txt","r",stdin);    //输入重定向,输入数据将从D盘根目录下的in.txt文件中读取
    freopen("D:\\out.txt","w",stdout);  //输出重定向,输出数据将保存在D盘根目录下的out.txt文件中
    while(EOF != scanf("%d %d",&a,&b))
    {
        // 把30,33,36分成三行写入out.txt中
        printf("%d\n",a+b);
    }

    return 0;
}

注意:如果是Linux/Ma

例3

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

int main()
{
    int a, b;
    freopen("D:\\in.txt","r", stdin);   //输入重定向,输入数据将从D盘根目录下的in.txt文件中读取
    freopen("D:\\out.txt","w", stdout); //输出重定向,输出数据将保存在D盘根目录下的out.txt文件中
    while(cin >> a >> b)
    {
        cout << a + b << endl;
    }

    return 0;
}

这里stdin和stdout包含在stdio.h中,所以得包含stdio.h或cstdio。

在windows系统中,控制台的名称为"CON", Unix/Linux系统中,控制台的名称为"/dev/console",所以随时可重定向回控制台

例4

#include <stdio.h>

int main()
{
    int a,b;
    // in.txt中有三行数:
    // 10 20
    // 11 22
    // 12 24
    freopen("D:\\in.txt","r",stdin);    //输入重定向,输入数据将从D盘根目录下的in.txt文件中读取
    freopen("D:\\out.txt","w",stdout);  //输出重定向,输出数据将保存在D盘根目录下的out.txt文件中
    while(EOF != scanf("%d %d",&a,&b))
    {
        // 把30,33,36分成三行写入out.txt中
        printf("%d\n",a+b);
    }

    freopen("CON","r",stdin);
    freopen("CON","w",stdout);

    // 在控制台输入两个数并输出结果
    scanf("%d %d", &a, &b);
    printf("%d", a + b);


    return 0;
}

三、ifstream和ofstream

ifstream和ofsteam是C++中标准的文件输入流和文件输出流。

例5

#include <fstream>
using namespace std;

int main()
{
    ifstream fin("../in.txt");  // ..表示上级目录,.表示当前目录
    ofstream fout("../out.txt");

    int a, b;
    fin >> a >> b;
    fout << a + b << endl;

    return 0;
}

四、总结

C/C++总共有五种输入输出方式
(1)C++控制台cin/cout
(2)C语言控制台scanf/printf
(3)C++文件ifstream/ofstream
(4)C语言文件fopen/fscan/fprintf/fclose
(5)C语言重定向freopen


关注微信公众号请扫二维码

 

posted on 2018-09-07 14:00  Alan_Fire  阅读(468)  评论(0编辑  收藏  举报