C++中的零散重要知识点

来源:互联网 发布:淘宝直播右下角的心 编辑:程序博客网 时间:2024/05/21 10:38

(一):类相关

  1. static 在类中的关键是用
    在类中的静态变量除了整型,枚举类型(枚举也是以整型存储的)的const类型可以在类中初始化。其他的都不能在类中初始化。(注意还必须是const类型才能)如果不是整型或枚举的const类型,那么都要在类外初始化。如:
    class A{    static int a;};int A::a=1;

    其中类的静态成员初始化不受访问权限的控制。但以后的数据改变收到相应的访问权限限制。而且需要重点指明的是,其实类A成员a真正被编译器认为是定义的地方是其类外的的初始化。所以可以存在这样的静态数组声明:
    class A{    static int line[];}int A::line[]={1,2,3};

    在类中可以指明大小,而在其初始化出指出。这也正是MFC类声明的消息网络建立的宏所采用的技术。
    #define DECLARE_MESSAGE_MAP()\static AFC_MSGMAP_ENTRY _messageEntries[];//我断章取义只写一部分。

    定义一个未知长度的消息入口结构体声明。而在cpp的宏实现完成相应初始化。
    另外类的引用数据成员只能通过构造函数的初始化列表进行初始化,因为引用数据成员必须在创建的时候初始化,而初始化列表正是创建成员变量刚完成的时候。

  2. 类对象初始化
    类声明时用等号赋值,调用相应的单参数构造函数。

  3. 没有成员变量的类大小
    如果我们申明一个纯空的类,那么这个类的大小是4。因为编译器会为这个类添加一个字节的变量,以使得这个类对象在内存中有唯一地址。但是不是没有成员变量就是空类了呢?这经常会引起误会,其实没有成员变量也可能会有类大小,那就是虚函数表指针。所以当一个类没有成员变量,但有虚函数时,不要以为其大小是8(一个虚表指针,一个编译器给的一个字节变量),而应该是4(仅有一个续表指针)。

  4. 类对象声
    有一种极其奇怪的类对象声明那就是构造函数加变量名。
    class A{     int a;public:    A():a(0){}    A(int b):a(b){}    void Show() const {cout<<a<<endl;}};

    一般声明:A a;
    奇怪声明:A(a);

    奇怪声明只限于单行单语句形式。所以下面的语句不是声明a,而是用a构造一个b。
    A b=A(a);//这是构造b。

    没有什么好讲,就和怎么声明变量一样,C++有这么一个语法。所以完全可以

    A(a);
    a.Show();

  5. 基类的构造和析构函数中一定不要直接或间接的调用的虚函数。
    因为在基类构造函数中派生类还没构造出来,所以此时对象的类型可以认为是基类类型,所有与类型相关的东西统统不管用了,包括类型鉴别和虚函数等。同理在析构函数中也一样,因为析构是从派生类逐渐往上到基类,所以到基类析构时调用虚函数已经没用了,派生类的部分已经失去作用了。虚函数等东西也肯定功能全都丧失!

    所以也有人说不要在任何类的构造函数和析构函数中使用虚函数。

  6. 基类指针调用一个派生类中虚函数(此函数再基类中是非虚函数),最终会调用基类的非虚函数

    class A{public:void Test(){cout<< "A::Test" << endl;}};class B : public A{public:virtual void Test(){cout<< "B::Test" << endl;}};A* pA = new B();pA->Test();
    因为这个在编译阶段就已经决定了,编译器发现A类中这个函数是非虚函数,所以根本不会查虚表。

  7. 虚函数完成运行时的类函数多态行为

    但是我们想这样一个问题?

    如果我们想我们的类层次结构实现一个函数多态,那么我们就会想着为我们类层次结构中的基类添加虚函数,然后派生类依次重写。

    但是不是只有直接调用虚函数才能实现多态行为呢?

     

    别忘了,我们函数之间是可以相互调用的,你也看到我说的是直接。所以,我们可以通过基类一个普通函数,让此普通函数调用虚函数,实现派生类直接调用基类普通函数实现多态。这些东西说起来很简单。但实际相灵活运用,并且用的恰到好处,那可是需要很高的水平啊。其实以上提的这两种方法本质都是利用了虚函数的多态行为。但是后一种也时常在实际的使用,而初学者也经常会误会,搞不懂其中的玄机。

     

    类切割注意点:

    class A{public: A(){cout<<"A's default construction is invoked!"<<endl;} ~A(){cout<<"A's destruction is invoked!"<<endl;} A( const A& rhs ){cout<<"A's copy construction is invoked!"<<endl;} virtual void Show() const{cout<<"A's Show"<<endl;}};class B:public A{public: B(){cout<<"B's construction is invoked!"<<endl;} ~B(){cout<<"B's destruction is invoked!"<<endl;} virtual void Show() const {cout<<"B's Show"<<endl;}};main(){    B b;    ((A)b).Show();}

    其输出时什么呢?

     

    答案是:

    A's default construction is invoked!
    B's construction is invoked!
    A's copy construction is invoked!
    A's Show
    A's destruction is invoked!
    B's destruction is invoked!
    A's destruction is invoked!

     

    原因是:((A)b)是C++重要的对象切割,这种切割会把b对象产生临时A类对象。是一个全新的对象,所以调用的虚函数是A类的。

     

    如果在main()

    {

        B b;

        ((A&)b).Show();

    }

    其输出时什么呢?

     

    答案是:

    A's default construction is invoked!
    B's construction is invoked!
    B's Show
    B's destruction is invoked!
    A's destruction is invoked!

    原因是:((A&)b)相当于把b传递给A&的一个匿名对象,由于引用所以没有创建新对象,还是原来对象的另一个别名,同时具有指针的多态性质。

     

    另外容易混淆的是:在类函数中以类作用域符号调用虚函数方式将不会有多态性之,而是单纯的调用对应类的对应函数。切记!



  8. 类中同一段(如public,protected,private)中的变量在内存中的顺序与书写顺序一致,但不同段之间的变量的内存相对顺序是未定义的。(Inside C++ Model一书中是这么说的,未曾验证)

  9. 虚继承
    1.虚继承是指使用virtual关键字去继承某类,主要是确保在多重继承中避免基类存在两份。

        class A { public: int a; };    class B : public virtual A   { public: int b; };    class C : public virtual A   { public: int c; };    class D : public B, public C { public: int d; };        D d;    int iSize = sizeof( d );

    上述代码中:iSize = 24(在Mac机器上做的测试,最近转了ios,一直用Mac)

    实现机制是,在高层的派生类中(d),只存留一份A。而在B和C中只存留A的相关指针(编译器编译时添加的,指针的值并不是d中的a的地址,而是类级别的地址,也就是说D的不同对象中这个指针的地址是一致的。具体实现目前不清楚)。详细讲解见《Inside the C++ object Model》

    2.有虚函数,有数据成员的的虚基类
    class AX    {    public:        AX(){std::cout<<"A's default construction is invoked!"<<std::endl;}        ~AX(){std::cout<<"A's destruction is invoked!"<<std::endl;}        virtual void Test() {}        int a;    };        class BX:public virtual AX    {    public:        BX(){std::cout<<"B's construction is invoked!"<<std::endl;}        ~BX(){std::cout<<"B's destruction is invoked!"<<std::endl;}    };    int iSizeBX = sizeof( BX );


    此时的iSizeBX是多少呢?答案是:12。C++标准要求虚基类的直接派生类,要添加虚基类表指针,以方便定位虚基类在派生类中的位置。(在很多编译器里面,虚函数表和虚基类表是首尾相连的。所以这个虚基类表指针可以当虚函数表指针使用。)所以BX以一个虚基类表开头,其后跟着虚基类。虚基类本身大小是8,所以总大小是12。

    3.没有数据成员,只有虚函数的虚基类
        class AX    {    public:        AX(){std::cout<<"A's default construction is invoked!"<<std::endl;}        virtual ~AX(){std::cout<<"A's destruction is invoked!"<<std::endl;}    };        class BX:public virtual AX    {    public:        BX(){std::cout<<"B's construction is invoked!"<<std::endl;}        ~BX(){std::cout<<"B's destruction is invoked!"<<std::endl;}    };    int iSizeBX = sizeof( BX );

    iSizeBX的大小是多少了?答案很出乎意料:4.为什么是4呢?为什么不是8呢?我一开始以为是8,因为AX本身大小是4(虚表指针),BX本身需要添加(虚基类表指针),所以4 + 4 = 8.但是编译器本身就会对没有数据成员的虚基类进行特殊处理,估计就不需要保留虚基类表了,因为没有数据成员,不用保留了。

    4.空的虚基类
        class AX    {    public:        AX(){std::cout<<"A's default construction is invoked!"<<std::endl;}        ~AX(){std::cout<<"A's destruction is invoked!"<<std::endl;}    };        class BX:public virtual AX    {    public:        BX(){std::cout<<"B's construction is invoked!"<<std::endl;}        ~BX(){std::cout<<"B's destruction is invoked!"<<std::endl;}    };    int iSizeBX = sizeof( BX );
    iSizeBX是多少呢?仔细想想。答案是:4。可能你猜对了,如果没有,看看后面的解释。我一开始以为是8,因为我认为因为AX没有数据成员也没有虚函数,所以AX的大小是4(编译器添加的一个字节,以及三个字节的内存填充)。BX虚继承于AX,因为AX没有虚表指针,所以BX要自己添加虚表指针。另外BX中包含AX,所以一开认为BX大小是4(续表指针) + 4(AX大小)。但后来编译运行,发现iSizeBX是4。为什么呢?我想了想,自己给出了下面的解释,编译器是给类添加额外一个字节,当且仅当发现这个类大小为0时,所以当编译器在处理BX的时候,发现BX已经是4个字节了,编译器就不会在添加4个字节在BX的末尾了。而且子类完整性并不适用于编译器额外添的为了表明内存唯一的1的大小的字节。

  10. 类成员偏移地址
    自大学到现在用C++都四年了,居然现在才发现原来的类成员的偏移量(相对于对象首地址)可以通过简单的C++语句获得。
        class X    {    public:        X(){}        virtual ~X(){};    public:        int a,b,c;    };    int X::* p = &X::a;//p is four, which indicating the offset of a to the start address is four bytes.


  11. 虚函数重载
    虚函数重载并不要求重写的函数与基类中的函数一模一样,返回值是可以不一样的,但返回的类型必须是隶属于同一个继承体系的。

  12. 构造函数内的虚函数调用
    C++标准要求,构造函数内的虚函数调用,以构造函数所在属的类为基础,进行虚函数调用。也就是说构造函数内的虚函数是其作用的,但是是相对与当前已构造完成的类对象为基准进行调用的。如下代码:
    class Base1    {    public:        Base1()         {            Test( );//控制台输出Base1::Test()        }        virtual void Test(  )        {            CCLog( "Base1::Test()" );        }    };        class Derive : public  virtual Base1    {    public:        virtual void Test( )        {            CCLog( "Derive::Test()" );        }    };    Derive oDerive;

  13. 类对象的声明需紧挨着其使用代码
    以前记得是《C++ Primer Plus》还是其他的C或者C++基础书籍教育我们要养成良好的编程习惯,其中有一条是把需要的变量都声明在函数的最前面(应该是C的某本教材),这样程序看起来很清晰。但是最近看了《Inside the C++ Object Model》以及自己做实验确实发现这样的习惯在对于类对象来说是不好的。看下列代码:

    class GameObject{public:    GameObject() : m_strName( "None" ), m_iState( 0 ){}private:    std::string m_strName;    int m_iState;};void Bumpup( GameObject* pPlayer ){    GameObject goTemp;        if( !pPlayer )    {        return;    }        //GameObject goTemp;    //....        return;}

    如果我们把goTemp放在函数最上头,那么即使当pPlayer是NULL,我们不需要对goTemp做任何操作时,我们的goTemp也执行一次构造和析构。看似没什么影响,但是一个string的构造和析构就意味着一次malloc和free啊。这些操作对于效率要求比较高的函数是致命的。所以,我们现在建议让类对象(一般的int,float无所谓)的声明紧挨着其使用代码,这样的使用能让函数更高效。当然,如果函数实在不在乎效率了,或许为了美观,我们可以重返C风格。

  14. 析构函数是普通函数,类对象或者对象指针可以直接调用
    习惯了delete调用析构函数,或者编译器帮我们调用析构函数,自己基本上没调用过几次,但对于重温了布局new的用法了,突然一想,有没有与布局new对应的布局delete呢?后来发现没有,delete必须要用于删除内存。想只调用析构函数,我们直接调用就行了。所以要记住,析构函数是可以随便调用的,但构造函数是真的不能直接调用。

  15. 赋值和声明放一起效率更高
    以前养成了良好“习惯”(变量都放在函数前头)看来还会带来另外一些问题。如下面代码:
     
    T a, b;T c = a + b;
    上面代码假设T有operator+( const T& )。那么T c = a + b;在拷贝构造函数存在时,会开启NRV(Named Return Value)优化,NRV能让返回值不产生中间变量,以节省中间变量的生成和销毁。但如果不在声明是赋值,如下列代码:

    T a, b, c;c = a + b;
    无论你是否有拷贝构造函数,都无法开启NRV。而且一定会产生中间变量,也就会产生中间变量的构造和析构代价。原因是NRV要求返回值的接受对象(也就是c)必须是“干净”的,也就是未曾构造过的。因为NRV在内部会调用c的构造函数,所以如果你的c提前声明了,那么也就已经构造,正常来说就不应该再构造了。所以NRV无法开启。(具体的更为详细的解释,还是看《Inside the C++ Object Model》)

  16. 模版类指针的声明不会让模版类实例化
    当我们使用下列语句时,模版类并不会被实例化出来:
    Point< float > *ptr = 0;

    因为编译器不需要知道Point的定义,就可以定义指针了。但是模版类引用肯定会引发模版类实例化,因为引用一定会指向已经存在的对象。

  17. EH( exception handle )
    EH不但能展开函数堆栈(反向撤销函数堆栈),并析构这些函数的所有已构造的局部变量,而且更重要的是,如果异常在类的构造函数中抛出,那么EH保证调用该函数析构函数,并析构已经被构造的所有子对象和成员对象。根据上面几个特点,如果我们打算使用EH,那么我们对资源的管理最好用一层类来封装,如加锁。我们建立一个类,构造函数加锁,析构函数解锁。还有智能指针,能保证已分配的内存,在发生异常是,能在类的析构函数中内释放。

  18. dynamic_cast用于引用变量
    我们都知道dynamic_cast是一种安全的指针类型转换函数,且它同样可以用于引用变量。但是引用变量和指针有个最大的区别就是,引用变量不能指向空,一旦有引用变量,这个引用变量一定要指向一个存在的对象。所以将dynamic_cast用于引用变量是要特别注意,如果转换失败,那么会抛出 bad_cast 异常。

  19. 公有继承,保护继承,私有继承
    真的十分惭愧啊,学C++四年了,居然这三个概念一直不明朗。今天(2013-1-18)后然间看到一段代码后回想起,继承同样有共有,保护,似有三种继承。于是这三个概念就必须要在此时此刻搞明白。稍微看了一下别人的博客,感觉基本明白了。为了让自己更清晰的记住便将其记录在自己的博客内。

    公有继承:基类的公有成员和保护成员(这里的成员包括函数和变量)的访问属性将在派生类中保持不变。基类私有的在所有继承形式中对派生类来说都是无法访问的。
    保护继承:基类的公有成员和保护成员(这里的成员包括函数和变量)的访问属性将在派生类中变成保护成员。
    保护继承:基类的公有成员和保护成员(这里的成员包括函数和变量)的访问属性将在派生类中变成私有成员。

    上面讲的是本质特点,这些改变将引起派生类对象,在保护和私有继承下,无法访问基类的所有接口。这基本上就是全部了。

  20. operator ->
    虽然知道成员操作符->可以被重载,但是一直没用过,直到看《Design Patterns》的iterator模式时才发现这个操作符函数。一开始很疑惑,认为X* operator->()函数,需要额外再调用一次成员操作符->才能调用X*的函数。但实际上不用,或许这是C++内部实现的一种折射吧。给出个示例吧:
        class Object    {    public:        void Test(){ std::cout << "Object::Test()" << std::endl; }    };        class Proxy    {    public:        Proxy( Object* pA ) : m_pA( pA ){}        Object* operator -> (){ return m_pA; }    private:        Object* m_pA;    };        Object o;    Proxy p( &o );    p->Test();

  21. 虚函数机制不受访问属性限制

    当基类有个public的虚函数,派生类重载这个虚函数,但是将其放在protected里面,那么基类指针指向派生类对象调用次虚函数时,到底调用的是谁的呢?
    答:从《Inside The C++ Object Model》一书可知,一旦使用基类指针调用函数时,编译器首先查看基类的此函数是否是虚函数,如果是,将改写此据代码,将其通过虚表指针来调用函数。所以呢,这实际上已经确定了,将会调用派生类的虚函数了,因为此虚表指针已经指向派生类的虚表了。里面基类的虚函数地址已经被改写成派生类的地址了。

    当然,如果基类声明此虚函数不是public,那么编译器就会报错了。这种情况十分明显。不属于我们讲解的问题范围。

  22. 成员函数隐藏规则

    用了C++ 5年了,我才知道还有一个成员函数隐藏规则。即当派生类重载了基类的某个成员函数(仅函数名相同,参数相同不相同都可以),那么用派生类对象,将只能调用派生类的此函数,无法调用到基类与此函数同名但不同参的函数。如:
    class A{public:    void Test( int ){ std::cout << "A::Test( int )" << std::endl;    void Test() { std::cout << "A::Test()" << std::endl;};class B : public A{public:    void Test( int ){ std::cout << "B::Test()" << std::endl;};B b;b.test(); //编译器报错,找不到此函数。


(二)非类相关:

  1. printf罕见的错误:

    printf("%f %f",2,2.0);
    输出为0.0 0.0

    原因是以浮点数的内存格式读取整数2,然后输出为0.0。同时如果第一个%f出问题的话,会影响第二个%f的分析,因为字符串后面的参数是放在栈里面处理的。前面字节数读取出错会导致栈指针指向出错。

  2. char数据类型,少有的编码赋值方法,/0是八进制,/x是十六进制
    char='/065';char='/x65';


  3. 变量命名的编码命名,主要是用于无法表示的扩展字符集
    int k/u00F6rper;//00F6是o上面再加两个点的一种字符

  4. 永远不要在构造函数里面调用虚函数
    因为构造函数没有执行完,成员初始化不完成,这个时候链表可能不完整,所以可能会产生意想不到的后果。

  5. char类型转换成int
    char类型转换成int时,默认是进行符号转换,也就是说要判断被转换的变量的符号,因为char是有符号的,虽然我们一位是无符号的,不要忘了,我们C++有unsigned char类型呢?所以记住了,char是有符号的,如果 char c=129;int a=c;那么a就是个负数了,因为c的第一位是1,所以为了避免这样的情况,我们使用更安全的转换int a=(int)(unsigned char)c;其实int可以省略。

  6. const_cast的使用
    const_cast不可以直接去掉变量或对象的const属性。

    const int NUM=50;const_cast<int>NUM=50;
    这样是不行,不能直接去掉对象的const属性。

    但可以将用const_cast将const对象转换成非const的类型指针或引用,用这个指针可以改变该const对象的值。如:

    const int NUM=50;const_cast<int&>(NUM)=40;//或者*(const_cast<int*>(&NUM))=40;

    但有些地方需要注意,如一下语句:

    const int NUM=50;const_cast<int&>(NUM)=40;int arrInt[NUM];

    这里arrInt数组的长度是50不是40.因为数组分配内存是在编译阶段,而NUM值改变是在运行阶段。

  7. const_cast注意事项
    在类中不能如果想用

    static const int NUM=10; int arr[NUM];

    然后想通过一个函数方法改变NUM的值是不行的。具体原因或许可以说const_cast<in&>是不能改变静态常量的值,或许就是编译器就不支持。
    总结一下,全局分配的const变量不能被const_cast修改。

    个人建议除非万不得已不要用const_cast。

  8. 常量重叠
    常量重叠有两种意思。

    1:指多个常量对一个操作符而言是静态和无副作用的,那么编译器就是会在编译时期进行常量重叠,也就是直接将多个常量值经过提前计算得出新常量,而覆盖原来的常量。
    2:所有用使用const 常量的地方将直接在编译时期替换成其对应的字符值。但常量有自己的内存,只是编译器优化了不知从内存中取值,而是直接是进行值代换。

    所以会有很多看似诡异,实际情理之中的事情。例如:
    int main(){const int n = 2;int* p = (int*)&n; *p = 3;cout<<n<<endl;cout<<*(int*)&n<<endl;return 0;}

    我在VS2008的输出结果为:
    2
    3
    所以修改常量的实际作用不大,只要单单只出现常量名字的地方都进行了常量重叠。

  9. volatile
    volatile就是不稳定,易变的。第8点提到常量重叠,其中第二点提到了,为什么第8点里面的代码cout<<n;输出的是2,而不是3呢,就是因为编译器优化了不知从内存中取值,而是直接是进行值代换。而volatile切好和这种优化对着干,加上这个关键字的变量或者常量,都指明改对象是易变,编译器看到该对象的使用时,应该从其内存中取值,而不是直接简单的值代换。

    将第8点的代码中:

    const int n=2;改成
    volatile const int n=2;

    就可以让结果都是3。
    但是不要想着可以用这样的常量当数组下标,用volatile修饰的对象已经就有变量特性了,编译器是不允许将这种对象作为数组下标的。我的网摘里面的讲讲volatile里面讲的更透彻。

  10. int&
    今天看到:

    float b=1.0f;int a=(int)b;int c=(int&)b;cout<<(a==c);

    结果是FALSE。
    原来int c=(int&)b。做的是这样一个操作:int c=*(int*)b;叫做以int的格式去解读b,所以编译器没有完成转化,而是原搬数据,有因为float的格式与int不同所以结果不对。

  11. strcat
    不要试图在一个字符串中间追加字符串,例如:

    char szFileName[] ="a.bmp";strcat( szFileName+1,".txt");

    结果是:"a.bmp.txt"
    一开始以为会将".txt"穿插在szFileName中间。但实际上因为追加函数自动查找字符串末尾,第一个参数只是给出了字符串的起始点,然后函数自动往后查找终点。

  12.  char * pname 和 char name[ 1 ]在结构体中的区别
    pname是一个指针,其占用4个字节(32位机器上)。而name数组只占用1个字节,可以存放数据,同时name可以当指针使用。name[1]通常用来保证数据连续的。
    例如:我们在读取一些块数据的时候经常会碰到这样的数据结构

    struct NetData{    int iSize;    char Data[ 1 ];};

    我们当然希望数据从文件中读取来的时候,有个指针能用来指向块中的数据。那么上面这个结构体就可以完成这个任务。

    NetData* pChunk = ( NetData* )malloc( iChunkSize );memcpy( pChunk, pFileChunkPoint, iChunkSize );//data operationpChunk->Data[ 0 ];

    当我们将块数据从文件中读取出来之后,我们的数据结构可以很方便的访问块中的数据。这就是char[1]的妙用。如果将char Data[ 1 ]换成, char*,你在用pChunk->Data[0]的时候就很有可能崩溃,因为此时Data是四个字节的指针,其地址已经被块数据覆盖,所以数据被当成地址去访问那个地址了,这样的后果很有可能会崩溃。

  13. realloc
    以前不敢用realloc,因为怕堆中没有可以引申的内存,但今天重温了一下这个函数,原来是自己弄错了,realloc只有在内存不足的情况下才会失败。即使传入的地址后面没有足够的内存延伸,realloc会试图重新分配一个新的足够的内存,所以一般会成功的。但需要注意一点,返回的地址不一定和原来的地址,所以所有保存过原来的地址的指针需要重设新地址才能保证正确。

  14. const char*, char* const, char const*区别

    const char*, char const*, char*const的区别问题几乎是C++面试中每次都会有的题目。 事实上这个概念谁都有只是三种声明方式非常相似很容易记混。 
    Bjarne在他的The C++ Programming Language里面给出过一个助记的方法: 把一个声明从右向左读。 

    char * const cp;  ( * 读成 pointer to ) cp is a const pointer to char 
    const char * p; p is a pointer to const char; 

    char const * p; 同上因为C++里面没有const*的运算符,所以const只能属于前面的类型。

  15. scanf巧妙用法
    转自:http://blog.csdn.net/yanxi10/article/details/6751447

    对于输入字符串还有一些比较有用的控制: 例如经常需要读入一行字符串,而这串字符里面可能有空格、制表符等空白字符,如果直接用%s是不可以的,于是有些人就想到用gets(),当然这也是一种选择,但是懂C的人基本上都知道gets()是一个很危险的函数,而且很难控制,特别是与scanf()交替使用时前者的劣势更是一览无余,所以gets()一般是不推荐用的,其实用%[^\n]就可以很好的解决这个问题了, ^表示"非",即读入其后面的字符就结束读入。这样想读入一行字符串直接用scanf("%[^\n]%*c",str);就可以了, %*c的作用是读入\n,否则后面读入的将一直是\n。所有对%s起作用的控制都可以用%[], 比如%[0-9]表示只读入‘0‘到‘9‘之间的字符,%[a-zA-Z]表示只读入字母, ‘-‘是范围连接符,当然也可以直接列出你需要读入的字符,上面读字母之所以用范围连接符是因为要输入52个字符太麻烦了,如果你只需要读"abc"里面的字符就可以用%[abc] (或者%[cab]、%[acb]、%[a-c]、%[c-a].....), 如果想读入某个范围之外的字符串就在前面加一个‘^‘,如:%[^a-z]就表示读入小写字母之外的字符上面

    这些用法其实可以有很多推广用法的,比如说你要处理下面的字符串 23 44r f30 88888,3245;34:123. 让你输出里面所有的数字,就可以用下面的代码:

    bool skip(){ scanf("%*[^0-9]"); return true; } int main(){int n; while (skip() && scanf("%d", &n)!=EOF) printf("%d\n", n); return 0; }

  16. const变量链接属性

    今天又弥补了一个C++的重要知识漏洞,const定义的变量如果放在全局范围(即函数外),我们能在另外一个文件引用吗?答案很难回答,因为这样提问是不清晰的。下面是const对全局变量带来的影响。

    const会将变量的链接属性内部链接属性(我们一般的全局变量默认是外部链接属性,默即int a = 10;和extern int a = 10;是一样的)即你定义的const全局变量,默认是无法从另外一个cpp链接并访问的。可以测试一下,会报链接错误。但这并不意味这const变量无法被其他cpp文件访问,只是我们需要显示将其链接属性改为外部,所以对于想将const全局变量供整个app访问的项目来说,需要在const变量前面显示的加上extern关键词。但其中const char*很有意思,const char* pString;这个变量的链接属性是外部的,不是因为const不管用了,二是因为指针根本就不是const,什么意思呢?const char*是指向常量的指针,指针本身不是常量,即指针本上不是const的。所以说const char* pString;是全局链接属性,如果想将其改为内部链接属性,我们需要const char* const或者char* const。将指针本身改为常量属性。

    一般const int, const float变量都不会被分配内存,直接在编译时期将变量出现的地方替换成对应的常量,这也就是常量叠加。但是如果对const修饰的变量取地址,那么编译器就会为其分配内存了。相同的值可能只分配一处空间(至少VC是这样的)。


原创粉丝点击