• 由endl对printf和cout的思考


    【前言】二者的区别就不介绍了。二者使用方法:

    printf("%s",a);
    
    cout<<a<<endl;
    

       endl的作用是什么?

    一、endl作用

      众所周知,endl有一个换行的作用,第二个作用就是清空缓冲区buffer。

      为什么要清空缓冲区呢?

      首先思考缓冲区存在的作用,缓冲区的作用一是为了避免频繁的I/O操作对磁盘的损耗,二是减少存取时的函数调用的损耗。所以,c++里面的缓冲区意义是非常大的,注意printf是没有缓冲区的。

      我们上面即使没有加上endl,输出也会马上输出出来,那清空缓冲区的作用在哪呢?其实,我们这个程序太简单,不能体现。但是cout什么时候清空缓冲区我们是不确定的,这对于多线程来说增加了不可控,多线程只有栈区独享,所有线程共享原主进程的缓冲区,如果不及时清空缓冲区可能会造成串数据;cin更是从缓冲区读取数据,其中很多细节。

    二、cout和printf时间复杂度比较

      cout是对"<<”运算符的重载。其运行时间也慢于printf。有人做的测评:https://blog.csdn.net/eternity666/article/details/78001513

      在vs下cout是对printf的封装,还要保持同步。但是在g++下不是,有人做的测试,g++的cout速度反而比printf快10倍左右。https://blog.csdn.net/qq_26398495/article/details/55105094

      所以,在linux端编程,用cout就可以,没必要用printf.,而且更加明晰。

    三、printf怎么实现“C多态”?

      今天有个同学考我一个题目,很有意思。我们知道printf的参数列表,参数个数是不确定的,这就很类似C++里面的多态,那printf是怎么实现C“多态”的呢?

      我第一次思考这个问题,就先写下当时怎么思考的吧?当然,他的问题是没有回答上来。

      T1、C++里面的多态是怎么实现的?

      C++是静态编译型语言,所谓的静态联编,但是使用虚函数即可实现动态联编,类似动态链接库。实现方法就是编译器提前生成vptr指针,运行的时候在通过指针找到虚函数表,找到函数入口地址。从指针的角度,好像和C多态有点关系,用函数指针?

      顺着这个思路,我们知道,多态有编译期多态和运行期多态。编译期->重载,运行期->重写。

      T2、重载重写(重定义?)三者的区别 
           (1)成员函数重载特征:
          相同的范围(在同一个类中);
          函数名字相同
          参数不同;
          virtual关键字可有可无;
      (2) 重写(覆盖)是指派生类函数覆盖基类virtual函数,特征是:
          不同的范围,分别位于基类和派生类中;
          函数的名字相同;
          参数相同;
          基类函数必须有virtual关键字;
      (3)重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
          如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏;
          如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。
      (条款36:不应该重定义任何非虚函数,既然需要你重定义,那为什么不直接在基类定义虚函数?非虚函数就是表明此函数具有“不变性凌驾特异性”,所以尽量不要重定义)

      T3、C++里面的运行期和编译期多态

      (1)运行期多态(重写)

      运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。典型地我们会举下面这个例子:

    class Animal
    {
        public :
        virtual void shout() = 0;
    };
    class Dog :public Animal
    {
        public:
        virtual void shout(){ cout << "汪汪!"<<endl; }
    };
    class Cat :public Animal
    {
        public:
        virtual void shout(){ cout << "喵喵~"<<endl; }
    };
    class Bird : public Animal
    {
        public:
        virtual void shout(){ cout << "叽喳!"<<endl; }
    };
    
    int main(){
        Animal * anim1 = new Dog;
        Animal * anim2 = new Cat;
        Animal * anim3 = new Bird;
        //藉由指针(或引用)调用的接口,在运行期确定指针(或引用)所指对象的真正类型,调用该类型对应的接口
        anim1->shout();
        anim2->shout();
        anim3->shout();
        //delete 对象
        ...
        return 0;
    }                
    

      运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。

      总结:运行期多态通过虚函数发生于运行期

      (2)编译期多态(重载)

      对模板参数而言,多态是通过模板具现化和函数重载解析实现的。以不同的模板参数具现化导致调用不同的函数,这就是所谓的编译期多态。相比较于运行期多态,实现编译期多态的类之间并不需要成为一个继承体系,它们之间可以没有什么关系,但约束是它们都有相同的隐式接口。我们将上面的例子改写为:

    class Animal
    {
        public :
        void shout() { cout << "发出动物的叫声" << endl; };
    };
    class Dog
    {
        public:
        void shout(){ cout << "汪汪!"<<endl; }
    };
    class Cat
    {
        public:
        void shout(){ cout << "喵喵~"<<endl; }
    };
    class Bird
    {
        public:
        void shout(){ cout << "叽喳!"<<endl; }
    };
    template <typename T>
    void animalShout(T & t){
        t.shout();
        }
    int main(){
        Animal anim;
        Dog dog;
        Cat cat;
        Bird bird;
        animalShout(anim);
        animalShout(dog);
        animalShout(cat);
        animalShout(bird);
        getchar();
    }
    

        在编译之前,函数模板中t.shout()调用的是哪个接口并不确定。在编译期间,编译器推断出模板参数,因此确定调用的shout是哪个具体类型的接口。不同的推断结果调用不同的函数,这就是编译器多态。这类似于重载函数在编译器进行推导,以确定哪一个函数被调用。

      (3)二者的区别:超链接

      T3、那么C实现“多态”是和C++ 类似的吗?

      (1)重载实现(编译期)

      看printf的源码:

     1 #include <stdio.h>
     2 #include <stdarg.h>
     3  
     4  
     5 //va_start(arg,format),初始化参数指针arg,将函数参数format 右边第一个参数地址赋值给arg
     6 //format必须是一个参数的指针,所以,此种类型函数至少要有一个普通的参数, 
     7 //从而提供给va_start ,这样va_start才能找到可变参数在栈上的位置。 
     8 //va_arg(arg,char),获得arg指向参数的值,同时使arg指向下一个参数,char用来指名当前参数型
     9 //va_end 在有些实现中可能会把arg改成无效值,这里,是把arg指针指向了 NULL,避免出现野指针 
    10  
    11  
    12 void printf(const char *format, ...)
    13 {
    14     va_list arg;
    15     va_start(arg, format);
    16  
    17     while (*format)
    18     {
    19         char ret = *format;
    20         if (ret == '%')
    21         {
    22             switch (*++format)
    23             {
    24             case 'c':
    25             {
    26                         char ch = va_arg(arg, char);
    27                         putchar(ch);
    28                         break;
    29             }
    30             case 's':
    31             {
    32                         char *pc = va_arg(arg, char *);
    33                         while (*pc)
    34                         {
    35                             putchar(*pc);
    36                             pc++;
    37                         }
    38                         break;
    39             }
    40             default:
    41                 break;
    42             }
    43         }
    44         else
    45         {
    46             putchar(*format);
    47         }
    48         format++;
    49     }
    50     va_end(arg);
    51 }
    52 int main()
    53 {
    54     printf("%s %s %c%c%c%c%c!
    ""welcome""to"'C''h''i''n''a');
        printf("%s %c","abc","c"
    );//重载了使用
    55     system("pause");
    56     return 0;
    57 }

      void printf(const char *format,...);。注意到"..."这个可变参东西了吧,printf的“...”内部实际上是通过 __VA_ARGS__ 这个宏来实现的:链接。他有三个参数,具体可参考:这里

      (2)重写(运行期)

      这个就要通过函数指针了吧:https://blog.csdn.net/philip_puma/article/details/25973139

     

     

  • 相关阅读:
    《梦段代码》阅读笔记03
    用户场景
    冲刺!
    冲刺!
    周总结8
    冲刺!
    冲刺!
    PMS权限管理和鉴权过程
    PMS构造函数以及apk如何扫描
    PMS的学习之旅
  • 原文地址:https://www.cnblogs.com/huangfuyuan/p/9513826.html
Copyright © 2020-2023  润新知