码迷,mamicode.com
首页 > 编程语言 > 详细

C++ 笔记(待续)

时间:2015-10-01 16:34:08      阅读:286      评论:0      收藏:0      [点我收藏+]

标签:

本文摘自:Thinking in C++ Vol.1  (添加部分C++ primer内容。待续...)

第零章:help and tips [Index]

安装c/c++的帮助文档:
yum install man-pages libstdc++-docs

  • tips
    • 应当将修饰符 * 和 & 紧靠变量名 ,因为修饰符优先和变量名结合 在int *x, y; 中x是指针,y是整型变量。
    • 静态变量加前缀 s_(表示 static)
    • 如果不得已需要全局变量,则使全局变量加前缀 g_(表示 global )。
    • 类的数据成员加前缀 m_(表示 member)或者在成员变量后加 _,这样可以避免数据成员与成员函数的参数同名。
    • cin cerr clog .cerr 是标准错误,clog是用于输出程序运行时出现的一般信息,如程序的日志文件。cin 从标准输入中获取数据,从左到右返回标准输入中的数据。//设输入是:1 5 1 3 7 1 9 cin>>a>>b>>c>>d>>e>>f>>g; 则a = 1 b = 5 c = 1...
    • endl 被称为操纵符,写入endl的效果是介绍当前行并且将与设备关联的缓冲区中的内容刷入到设备中。这可以保证所有需要显示的数据已经写入输入流而没有保存在内存中。
    • 字面值,只能使用其值来称呼它。
    • 整数的后缀:u或U表示无符号型。浮点的后缀L表示long double (C++11特性)
    • 初始化和赋值的区别:
      • 初始化:创建一个变量时赋予的初始值。
      • 赋值 :把变量当前的值擦掉,写入一个新的值。
    • 使用花括号对变量进行赋值:(C++11特性)

      例如:int i{7};特点:使用花括号对变量进行赋值时,当存在丢失数据的风险时,编译器会报错:int i{3.14};是无法通过编译的,因为会丢失数据。

构造函数和默认构造函数的一些区别

默认构造函数分为两类,一类是系统自动生成的,即此时类中没有定义构造函数。第二类是类的作者写的不带参数的构造函数或者带默认参数的构造函数。调用这类构造函数的标准语法是ClassName objectname;(注意这里是不带括号的)。其他的构造函数都是带有非默认参数的,这些构造函数的调用需要加上参数,而这类情况是比较常见的。在定义一个类的时候不能使用这种方式:ClassName object();即在对象名后加上一个空括号,在任何地方这种语法都会被编译器认为是声明一个没有参数且返回值是ClassName的函数(参考)。

  MyClass c1;//表示使用不带参数的构造函数,或者有默认参数值的构造函数。
    MyClass c2();//不会调用无参构造函数,各种情况下该处是声明一个返回值为MyClass类型的函数而已
    MyClass c3(1);//调用参数为int的构造函数
    /*---------------对于new关键字加括号和不加括号的区别---
    1.对于自定义类型来说没有区别,都是使用默认构造函数
    2.对于内置类型来说加括号会初始化
    */

返回值优化

在函数内部: return ClassName(paramaters);
ClassName tmp(paramaters);
return tem; 有着不同的含义,前者将直接在返回值地址上创建对像,只会调用一个构造函数。而后者会创建一个局部变量然后再返回,此时的开销要比前者大。这就是返回值优化。

第一章:对象 [Index]

聚合(aggregation)或组合:被认为是“有一个” , 就像“汽车有发动机”。 inheritance(继承):reusing the interface (接口的重用) 消耗较组合大。
当构建一个新类时你应该首先考虑组合(而不是继承),因为组合比继承更简单更灵活。

你有两种方法使你继承的类和基类不同:

  • 第一个方法很直接:简单的向基类中添加部分新的动作(函数)。
  • 第二中方方法更重要:改变基类中某些函数的动作(或功能),这被称为重载基类中的函数。
    在继承类中定义一个新的函数(一般与基类中的函数同名),就可以重载基类中所有的同名函数。

使用关键字virtual声明你想让一个函数具有晚绑定的属性。

  • 早捆绑:在编译时确定调用的函数(即编译时确定所调用的函数的绝对地址)。
  • 晚捆绑:在运行时确定所调用的函数的地址。

我们把处理继承类就像处理其基类的过程称为向上类型转换(upcasting)。

我们有两种主要的创建对象的方法:

  1. 在栈中的变量经常被称为自动变量或局部变量(在运行时分配与销毁)。静态存储区是一块固定的存储区域,在程序运行前就已经分配(与进程的生命周期相同)。
  2. 第二种方法是在被称作堆(heap)的内存池中创建对象(内存池由系统维护)。如果你需要一个新的对象,可以使用关键字new在堆中创建它。当你不再使用这个对象,必须使用delete销毁这块内存。

#include <iostream.h>
means
#include <iostream>
using namespace std;

八进制和十六进制:

  • cout << "in octal: " << oct << 15 << endl;
  • cout << "in hex: " << hex << 15 << endl;

容器 vector:
容器是模板,意味着它可以高效的应用于不同的类型。 vector就是一个容器(其结构类似于数组,左边是起始,右边是尾部)故使用push_back()添加新的数据。

    #include<iostream>
    #include<fstream>
    #include<vector>
    #include<string>
    using namespace std;
    int main()
    {
        ifstream in("test.txt");
        string str;
        vector< string > strvec;
        //while(getline(in, str)) strvec.push_back(strvec); // 每次读一行,下面这行代码,每次读两个空格之间的字符串,
        while( in >> str )strvec.push_back(str);                //以空格作为间隔读取数据,可能同时输出单词和符号,例如  .hello, 
            for(int i = 0 ; i <strvec.size();i++)cout<<i<<‘:‘<<strvec[i]<<endl;
            return 0;
    }

如果定义了一个数组而没有初始化,则编译器不会初始化数组。但若给出少于数组元素个数的初始化数据,则余下的数组元素会被初始化为0。故int b[n] = {0} ; 是将所有数组元素初始化为0 的简洁方法。

第二章:c in c++ [Index]

  • pass-by-value:按值传递,c中只有这一种传递方式,分为数据传递与指针传递(注意指针也是按值传递)。
  • pass-by-reference:按引用传递,c++独有,可在函数中直接使用内存中存在的变量。

引用的特点:

  1. 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
  2. 不能有 null 引用,引用必须与合法的存储单元关联(指针则可以是 null)。
  3. 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。这中操作是没有意义的,故非法。

强制转换:c++比c多了一种转换的语法int(data),在c中只能写成(int)data。

强制类型转换:

  • c可用: (int) i;
  • c++可用:(int) i; int(i);

c++的类型检查比c++严格:

int i = 10;
void *vp = &i;//c与c++均可
int *ip = vp;//c中可用,但在c++中不允许,在c++中不允许将void指针直接赋予其他类型的指针。

c++中不允许调用未事先声明的函数,但c可以。 c++中有显式运算符,例如:&&可以表达为 and

使用函数指针调用函数的正规语法:(*funp)(data),但大部分编译器允许程序员写作funp(data) ,但这只是一种简写的方式。

下面的#和##只能用于宏定义中:

#将变量或表达式转化为字符串。当你在宏变量前加上 # ,预处理器会把宏参数转换为字符串。 标志粘贴:##将两个标识符连接从而形成一个新的标识符。

在标准头文件中你可以发现assert(),这是一个方便的debug宏。当你使用assert()时,你提供一个参数,也就是一个你认为为真的等式(断言)。预处理器会生成一些代码测试这个等式,如果断言是假,程序会停止运行并给出断言所处的位置。 完成测试之后在代码的开始加上 #define ndebug 则可使assert()失效,从而提高效率。

函数的地址可以使用其函数名表示。 当然也可以使用更明确的方法,使用取址符号, &fun().

      int main()
      {
          int (*fp)(void);  //定义函数指针
          pr();
          fp = pr; 
          fp();                //这两个使用方法均是正确的,但后者更规范,fp()  这种定义是编译器提供的。
          (*fp)();    //   
      }

第三章:深入理解字符串 [Index]

C++标准规定字符串内存的分配允许但不要求使用引用计数。但无论是否使用引用计数,类的实现对用户而言必须是透明的。

  • 引用计数:

    在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。使用引用计数的一个优点是可以实现写时复制,这样可以节省时间和内存。但在多线程编程中几乎无法使用引用计数技术。

  • 字符串"\1234"所表示的是两个字符:\123为123所对应的字符unicode值,而4又是另一个字符的Unicode值(或者ASCII)。

  • 字符和字符串字面值:(具体解释见笔记:编程杂记)
    +--------+-----------+----------+
    | 前缀   | 含义      | 类型     |
    +--------+-----------+----------+
    | u      | Unicode16 | char16_t |
    +--------+-----------+----------+
    | U      | Unicode32 | char32_t |
    +--------+-----------+----------+
    | L      | 宽字符    | wchar_t  |
    +--------+-----------+----------+
    | u8     | UTF-8     | char     |
    +--------+-----------+----------+
    

第四、五章:实现的隐藏 [index]

第四章的部分内容: ::(作用域解析运算符):
指定所定义的函数所属的范围。stash::link::add() 说明定义的add()函数属于stash中的link结构。此操作符只用于类或结构,不能用于他们的实例,因为这是没有意义的,只用改变类或结构才会改变他们的实例(类和结构像模板,而实例相当于他们的拷贝,只有模板变量,拷贝才会变)。如果在变量或函数之前使用 ::说明函数或变量时是全局变量和函数,不是本类或结构体中的与全局变量同名的函数和变量。

c++访问控制:

  • class中默认的成员为private而struct中的成员默认是public。
  • public访问说明符意味着在其下的声明对所有人都是可用的。
  • private关键字从另一个方面来说,意味着除了类的创建者,其他人都不能访问私有的(private)内容。
  • protected的行为和private很像,但两者有一点不同:在继承中,私有的内容不能被访问,但protected内容可以被访问。

构造函数与析构函数均为public成员,因为析构函数与构造函数均由系统自动调用,而调用的方式是在对象定义处与包含对象的右括号处由编译器自动插入调用构造与析构函数的语句。此时这两个函数相当于在对象的外部执行,其必须为public。

可以通过在结构中声明友元实现让一个非成员函数更改某个结构中的成员。在所有关系中一个非常重要的规则是“谁能访问我的私有成员?

第六、七章:初始化和清除 [Index]

构造函数:

与类的名字相同,但没有返回值 。在创建对象时,由编译器自动的调用。编译器会在对象的定义处插入构造函数的调用行,编译器会隐含的向构造函数传递一个参数,即本对象的内存地址。 构造函数没有返回值是因为构造函数是由系统自动调用的,如果为其添加返回值,系统必须自动处理返回值,而这样做没有意义。

默认构造函数:

可以不显式的提供参数的构造函数,例如类中定义的无参构造函数和参数都有默认参数的构造函数,当然了还有系统自动生成的。当类中没有显式的定义构造函数,系统会生成一个默认的构造函数,但这个默认构造函数不会对对象中的数据做明确的初始化。

在析构函数内部使用exit函数是非常危险的,因为程序从main中退出时有时也是调用exit函数,如果在析构函数中使用exit函数将会造成死循环:例如程序从main中退出,系统会调用exit函数,这时系统会调用对象的析构,如果析构函数中存在exit函数那么系统会去终止main函数,而main中打exit触发后会再次去执行对象的析构函数,这样就形成了死循环。

析构函数:

析构函数与类的名字相同但在前面添加 ~ ,析构函数没有参数表(有参数表也没意义,因为析构函数由系统自动调用,有参数表可以完成的动作,没有参数表一样可以完成,有还是没有参数对析构函数而言是一样的),且析构函数由编译器自动调用。析构函数自动调用的唯一根据是包含这个对象的右括号。

因为析构函数与构造函数均由系统自动调用,而调用的方式是在对象定义处与包含对象的右括号处由编译器自动插入调用析构与构造函数的调用语句。此时这两个函数相当于在对象的外部执行,其必须为public。

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

局部跳转(goto)会可能触发析构函数(只要goto跳出了对象的作用域)。非局部跳转不会触发析构,例如:setjmp()和longjmp。构造函数与析构函数均为public成员。

c++中对象的定义和初始化是集为一体的:对象的创建和初始化必须同时完成,否则无法通过编译。一般编译器会检查是否在条件语句中定义了对象。 因为在条件语句中的对象可能没有被初始化:例如:if(*)static x x; if语句很可能因为假而被跳过,故对象x只是被分配了内存,但没有被初始化,此系统会报错或警告。

重载:

相同的名字但不同的参数表。不能使用返回值实现重载,因为很多时候我们使用了一个函数但忽略它的返回值。一般而言编译器内部实现重载的方法是使用参数列表来标识每一个函数,例如一个函数int print(float flt)在编译器内部可能表示为_print_float()。

union:

同一片内存空间,当选择联合中不同的成员时,会对这片内存做不同的解释。

匿名联合:
没有名字的联合,例如:

    union{
        float f;
        double d;
        int i;
    };

在使用这种联合时,不需要联合的名字就可以使用其中的变量,例如直接对i赋值:i=9; 可以用作浮点数的相等比较

占位符参数:

声明:void f(int x , int = 0 , float flt = 1);//默认参数只能存在于函数声明中。
定义:void f(int x , int , float flt ){}
调用:调用含有占位符参数的函数,必须为占位符参数提供一个参数,而且这个参数要与占位符类型一致。

函数的声明中 int fun(); 在c++与c中的含义是不同的,c中意味着函数的参数是任意的,类型任意,参数也任意。但在c++中表示没有参数。

static在不同范围的两种意思(这些规则对对象而言一样适用):

  1. 内部连接
  2. 这类变量会保存在静态存储区,而且这类变量只会被初始化一次。

const与volatile:
c++中与c中的const意义不同。在c中会为const变量分配内存空间,在c++中不一定会这样。在c++中const变量可能并不存在内存中,因为在编译过程中编译器可能直接将其优化而消失。

volitile 与const相反,volatile通知编译器不要优化某个变量。

位运算符 & | ~ ^(异或) << >>

注意:

所有的位运算都是在寄存器中执行的并没有在变量的内存空间有所体现。那么为了对某个变量进行位操作,必须进行赋值操作。例如 i = i>>2 ;

所有的二元运算符都可以和等号相结合如: a += 3 ; b>>=2; ~是一元运算符,不能与等号结合。

逗号运算符:

逗号运算符是所有运算符中优先级最低的。

    int main()
    {
        int a1 , a2 , b=2 , c=3 , d=4;

        a1 = (b++,c++,d++);
        a2 = b++ , c++ , d++;//赋值运算符  ‘ = ‘ 的优先级高于逗号故先赋值
    }

逗号运算符用于分隔表达式,而逗号运算表达式的值是最后一个表达式的值。上面a1的值为4整个表达式的值是最后一个即d++ 。 a2的值为2 ,等号的优先级要高于逗号运算符。

c++中的显式转换:

  • static_cast:用于非多态类型的转换。
  • dynamic_cast:用于多态类型的转换。
  • const_cast:用来消除const, volatile, __unaligned属性的转换。
  • reinterpret_cast:用于空间的重新解释。
    具体见:http://blog.csdn.net/callmeback/article/details/4040583

与c不同,c++中结构体的名字可以直接用来定义新的变量。

例如:定义struct s{};
在c中定义变量时需要使用:struct s vs;
在c++中可以直接使用: s  vs ;

关键字this产生本结构体的地址。

java中不允许局部变量与全局变量同名,但c++可以。

第八章:常量 [Index]

c中可以使用宏定义,例如:#define pi 3.1415
在c++中使用const替代 常量的宏定义,这样可以增加程序的安全系数。

c++中cosnt默认为内部连接 ,而在c中cosnt默认为外部连接。

书中说编译器无法获得内存中的数据,这只是对某些编译器成立,在vs2013中下面的代码1无法通过编译。说明vs2013的编译器无法在编译时获得array1[1]中的数据。但在g++编译器中代码1可以通过编译并且输出为8 。 //代码1

    int array1[] = {1, 2, 3, 4};
    int array2[array1[1]];

提示:error c2133: ‘array2‘ : unknown size 在vs2013中无法通过编译,

//代码2

    cout<<sizeof(array2);
    int b = 9;
    char c[b];

提示:error c2133: ‘c‘ : unknown size

//代码3

    const int b = 9;
    char c[b];

代码3可以通过编译,说明编译器对b做了优化(常量折叠)。

c中和c++中对const变量的定义有本质的差别:

  • c:一个不可以被改变的变量,这个变量无法直接更改,但可以使用间接的方法,c中常量一定会被分配内存空间(不在只读存储空间)。在 c 中const默认为外部连接。在c中可以声明:const int bufsize ;不初始化,但c++不行。
  • c++:一个常量,编译期间的常量。(const在c++中还有其他的意义,见下面)声明和定义一般必须同时完成(除了类中的常量),因为定义后无法更改。 在c++中const默认为内部连接。故需要加上关键字extern才能使其被外部文件连接。

c++中使用类似c中使用指针的方法改变const变量的行为没有定义(未定义行为),也就是更改结果与编译器实现有关。
例如:

  • 代码1: c++

    #include<iostream>
    using namespace std;
    #define pr(x) (cout<<#x##" = " <<x<<endl);      
    int main()
    {
       volatile const  int m = 9;
       pr(m)
       int *pint =(int *) &m;
    
         *pint = 6;
       pr(m)
       pr(*pint)
    
       system("pause");
    
    }
    
  • 代码2: c++

    #include<iostream>
    using namespace std;
    #define pr(x) (cout<<#x##" = " <<x<<endl);  
    int main()
    {
       const  int m = 9;
        //不一定开辟空间        
       pr(m)
        //强迫编译器为常量开辟空间
       int *pint =(int *) &m;
        
       *pint = 6;
       pr(m)
       pr(*pint)
    
       system("pause");
    
    }
    
  • 代码3: c

    #include<stdio.h>
    using namespace std;   
    #define  pr(x) {printf(#x);printf("=") ; printf("%d\n" , x);}
    int main()
    {
       const  int m = 9;
       //c编译器一定为常量开辟空间
       pr(m)
       int *pint =(int *) &m;
    
       *pint = 6;
       pr(m)
       pr(*pint)
    
       system("pause");
    
    }
    
  • 代码1 c++编译输出:m = 9 m = 6 *pint = 6
  • 代码2 c++编译输出:m = 9 m = 9 *pint = 6
  • 代码3 c编译输出:m = 9 m = 6 *pint = 6

c++中的常量是编译期间的一个定值,故编译器没必要为这个常量分配内存,而且const常量没有像字符串常量那样被分配在只读存储区。而且常量的值会保存在符号表中,编译器会将局部const变量进行常量折叠。故如果强迫编译器为常量分配内存,即使在c++中改变了常量在内存中的值,我们在代码中使用的常量的值依旧不会改变,故会出现上面代码2的情况。但是如果在常量的前面添加关键字volatile,告诉编译器不要对const常量进行优化即不做常量折叠,每次使用const常量时都从内存中读取,那么就会出现代码1中的结果。c中一定会为常量分配内存,而且每次使用时都会从内存中读取,故会出现上面的代码3的结果。从上面的结果可知,const常量没有像字符串常量那样被分配在只读存储区。

参考资料:http://blog.csdn.net/heyabo/article/details/8745942

    const  int *pint;  
    int  const *pint;      //这两个声明的意思是相同的,pint指向一个int型常量(这个变量中的值不能变)。
    int * const pint;       //pint是一个常量型指针(指针内容不可变),其指向一个int型变量。
  • 字符串是默认的常量并且保存在只读存储区,字符串中的数据是无法更改的(一般更改字符串的代码可以通过编译,但在执行时会触发硬件中断)。
  • 字符数组一般是变量,字符数组一般在栈中或静态存储区(静态存储区和只读存储区是不同的概念)保存,因为是变量所有其中的内容是可以更改的。
  • char *pchar = "hellow !"这样的代码在技术上而言是错误的,因为赋值号的右侧是字符串而左侧却是一个指针。但编译器允许这样,编译器默认将字符串的首地址赋予指针。

在pass-by-value形式的函数前添加关键字const是没意义的,因为完全没有必要,原来的数据一定不会被更改。

如果一个函数返回一个const型的对象,那么这个函数不能作为左值,因为其返回的内存空间是不能被更改的,更不可能被赋值。当一个函数返回的是一个对象而且这个对象不是const型时,这个函数可以作为左值,例如:f(x1)=f(x2),在c中是看不见这样的赋值形式的,在c中左值永远都是变量,而不可能是函数的形式(这里涉及了C++中的临时变量的概念)。如果一个函数返回的是内建类型,那么编译器不允许其作为左值。允许返回值为对象的函数作为左值可以写出链式的表达式例如:f(x).fun(y) ,其中fun( y)为f(x)返回值的一个成员函数。可以将上面的代码作为右值,这样可以简化代码。

类中的常量有两种形式:

  1. 对象中的常量:对于每一个对象而言某一个变量是常量,由同一个类创建对象的常量值不同,一般这样的常量在类中声明的形式为const int data;  必须在构造函数初始化列表中以类似fun(...) : data(9)...{ function }的形式初始化这些数据(必须以函数的形式初始化,不能用等号的形式)。
  2. 类常量:对于由同一个类创建的对象而言,每一个对象中这个常量都是相同的,这样的常量的声明形式如:static cosnt data = t;这样的常量在声明的同时必须初始化,也可以使用无标记枚举来实现这样的效果。这类常量只是在编译期间存在,因为编译器没必要为这样变量分配内存。

const对象和成员函数:
const对象只能调用对应类中的const成员函数。其中cosnt成员函数的定义,必须声明和定义的时候在参数表之后函数体之前添加const关键字。如果一个成员函数被定义为const类型那么在函数体内将无法更改对象中任何数据成员(除非这个数据被被关键词mutable修饰),否则将无法通过编译。

const成员函数的声明和定义

    class test
    {
        public:
        test(int a);
        void nchange(int b ) const; //const成员函数的声明
        private:
        const int m_a;
        int m_c;
    };

    void test::nchange(int b )const         //const成员函数的定义
    {
        //m_c = b;//无法通过编译
        int c = b;//c 不是类中的成员数据,故可以通过编译
    }

第九章:内联函数 [Index]

inline:

内联函数存在的理由:使用较小的空间代价获得较大的时间效率。

内联函数的函数体中不能出现循环和sewitch语句,否则编译器会自动将其转化为非内联函数。内联函数的函数名和函数体同时存在于符号表中。内联函数的函数体会嵌入调用内联函数的地方,而不会像其他函数那样使用call指令调用。

有两种情况编译器不会执行内联:

  1. 内联函数过于复杂,比如说内联函数中有循环或switch语句。
  2. 显式或隐式的取函数的地址。

内联函数的两种实现方法: 1、使用关键字inline声明函数。 2、在类中直接定义的函数,一般不推荐这种方法,因为这样做会破坏函数可读性。

c++规定只有在类声明结束后才会对内联函数进行计算。这样可以解决向前引用,即引用声明在自己之后的函数。

第十章:名字控制 [Index]

在c++ 中,那些常放在头文件中的名字如常量,内联函数默认是内部链接的。注:在C中常量默认是外部链接。

在程序的代码量达到一定的程度时,全局的名称就会发生重复,此时就需要名字空间来解决这个问题:

使用关键字namespace 来定义新的名字空间:

```
namespace mylib{
//...
}         //这里不加分号
```
  • namespace的定义和class,struct,union,enum不同,名字空间的定义后不加分号 ; namespace只能定义在全局中,但他们可以嵌套。
  • 名字空间的作用范围与普通的函数和类是相同的。若将名字空间定义在大括号内,那么名字空间的作用域就是这个大括号内部。
  • 名字空间和类的定义相同,在头文件中声名,在.cpp文件中实现。
  • 同一个namespace可以在不同的头文件中定义。但类是不可以重复定义的。
    namespace xalias = ...; //为名字空间添加别名。
  • 可以在一个名字空间中的类声名中嵌入一个友元声名。
  • 只要定义了名字空间(除了匿名名字空间),那么若想使用名字空间中的名字就必需显式的调用这个名字空间。

匿名名字空间:

    namespace{
        //...
    }
  • 如果把局部变量放在匿名名字空间中那么其为内部连接。
  • 这种名字空间中的名字在当前的编译单元是无限有效的,每一个编译单元只能有一个匿名名字空间。

使用名字空间的方法:

  1. 作用域解析法,类似于std::cout 优先级最高
  2. 使用声名,类似于using std::cout例如:下面的代码中mylib1和mylib2中都有name。使用声名获得名字要比使用指令方法引入的名字“优先级”更高。优先级第二。
  3. 使用指令,类似于 using namespace std ;使用指令可以使我们直接进入整个名字空间。优先级第三
     #include "fun1.h"
     using namespace std;
     int main()
     {
         {//名字空间的声名只给出了名字,没有类型信息,故声名引入了这个名字的重载集合。
         using mylib2::name;//使用声名,那么在后面使用的name都是来自mylib2中。若想使用mylib1中的name,必须完整的限定。
         using namespace mylib1;
         name();//这是mylib2中的name()
         mylib1::name();//完整的限定才能调用mylib1中的name。
         }
         salute salute("china");
         salute.sayhello();
        // name();
     }
    

类中的静态变量:

  • 类中的静态变量对于对像而言意味着所有的由这个类实例化的对像都共享这个static变量的存储空间。因为这个变量并不仅仅属于某个对像,故将初始化这类变量的语句放在类中不是最好的解决办法。
  • 类中static变量的初始化不能放在类的实例化时。必须在类的实例化之前对类中的static变量进行内存分配与赋值。
  • 静态数据的内存分配是在类的定义文件中实现的,如果没有在定义文件中定义静态变量,那么即使类中声名类静态变量,链接器会报错。具体的语法为全局:int a::i = 9;其中类a中定义类static变量static int i;
  • 注意static在编译单元中的意义,故在定义类中的static变量时不应再在变量前加上static关键字进行修饰。

在类中的常量(注意与对像常量的区别)有两种形式:匿名枚举和const static in a。在C++11中支持非静态变量的类中初始化。在C++11类的定义中出现int i = 2;是合法的,但在较低版本的c++标准是非法的。

局部类和嵌套类:

  • 局部类指在程序内定义的类,而嵌套类指类中定义的类。局部类不能有静态的数据,而嵌套类可以有静态数据成员。
  • 这是因为在对类中的静态数据进行初始化时必须在类的外边进行定义,然而局部类无法获得相应的作用域,故局部类不能有静态成员。

类中的静态成员函数:

  • 考虑在cpp文件中static的意义在于不允许其他的文件链接本文件中的名字,故不能在定义文件中再使用关键字static。编译器可以通过类中的声名知道哪些函数是静态的。
  • 类中的静态成员函数服务于所有本类的对像故类中的静态函数不能操作类中普通的数据和函数,因为系统不知到应该操作哪个对像的数据。
  • 虽然说类中的静态函数不能调用普通函数和普通变量。但普通成员函数和变量却可以使用静态的函数和变量,因为编译器知道访问什么。
  • 可以认为静态函数保存在一个独立的存储空间,故使用this关键字是无法引用静态变量和静态函数的。最好的解释是系统无法指定对像。
  • 类中的静态函数可以使用多种方法进行调用: 直接与类结合a::setstatic(...) 一般编译器也提供对像调用静态函数的语法支持即 . -> 可以利用静态数据的特点使某个类只能有一个实例。中文 thinking in c++:p233

全局变量初始化的相依性:(有相依性的全局变量存在的一些问题)
对于全局变量而言初始化顺序的不同会造成程序结果的不同,然而在不同编译单元中的全局变量的初始化次序是无法控制的。

在文件1中:

    extern int  y;    
    x = y + 1 ;

文件2中:

    extern int x
    y = x + 1 ;

编译器会默认将全局未赋值的变量初始化为0,如果上面的两个文件的编译顺序不同则x和y的值也将不同。在些程序时需要注意这些情况。

如果两个关联的全局变量无法避免,那么解决上面的问题有两种技术(技术2是最常用的):

这两种方法的核心思想是相同的:位于不同文件的有关联的静态数据要么不初始化要么就一起初始化,这样就可以限定初始化的顺序。

  1. 使用一个“哨兵”,使用类中的静态数据,这样就能通过哨兵确定是否所有相关联的变量已初始化,已初始化就就不用再初始化。例如:在类中设置一个静态的int变量这个变量的初始值是0,当初始化之侯,赋这个值为非零值这样就可以控制类的初始化次数。p235然而这个技术有个十分明显的缺点,那就是必须在有关联全局变量的文件中都定义一个这样的类。而下面这个技术就不用。
  2. 考虑static在程序内部的意义:函数内部的static变量只初始化一次。那么我们可以将所有关联的变量定义在函数内部,让函数返回引用。在引用这些变量时直接引用这些函数,因为这些变量在函数内部是静态的故只会初始化一次,由函数返回引用来调用这些变量。使用函数来调用这些变量,即使变量没有初始化,函数会自动的初始化这些变量。

    在两个文件中定义两个相关联的int 型数据int x 和 y 为了强制化调用次序,我们定义两个函数:

    • 文件1:int &x(){static int x = 1;return x;}
    • 文件2:int &y(){static int y = 8;return y;}
      在不同的文件中使用x和y时就使用这几个函数,那么变量的初始化次序只与代码有关。那么这些变量的初始化次序是可控的。

替代连接説明:
C++ 和C 在编译时函数的名字的修饰方法是不同的,故在c++中使用c库时需要说明:extern "C" float foo(...)或者extern "C" { ... }

第十一章:引用和拷贝构造函数 [Index]

引用就是能自动的被编译器间接引用的常量型指针。引用可以用于函数的参数表也可以用于函数的返回值。编译器会对常量进行常量折叠。

  • 引用的一般格式:int &data =... ;函数返回引用的格式是:return data(不再使用&,否则编译器无法解释&是取地址或其他)。引用的特点见第二章。
  • 如果代码中明确声名返回了一个引用那么这个变量所分配的内存空间接近于堆的内存空间,否则变量内存一般在栈中分配。

在C中允许将void 类型的指针赋予其他类型的指针,但在C++是不允许的。

如果确定不改变引用的数据,最好声名引用为常量引用,这样函数的应用范围更广。void func(const int &data);

指针引用:

  • 在C中如果想改变指针中的数据,我们一般声名一个指向指针的指针 例如:int **pIntPtr; 而在改变指针内容的时侯使用的语法一般类似:*pIntPtr = ...;
  • 如果我们使用的是指针的引用那么使用时语法就简单一些:int &*pIntPtr; 直接使用 pIntPtr = ...

函数框架:
在C/C++中参数表的入栈顺序是由右到左。

    int f(int x , char c);    
    int g = f(a , b);

对应的汇编类似于:

    push b ;参数入栈,右到左                                    
    push a  
    call f() ;                                                      
    add sp 4 ;清理栈中的数据    
    mov g , register a

对于内建类型,编译器知道类型的大小,且编译器可以将内建类型放入寄存器中进行计筭与返回。故f()的返回值在寄存器中。对于一般的函数调用,如果函数中只有内建类型,那么内存中函数框架类似于:

....栈顶|...函数参数...|返回地址|...局部变量...|栈底....

编译器对内建类型函数的调用过程:

  • 编译器先将f()中的参数由右到左压栈。这是框架中的函数参数。
  • 再将调用函数f()处的地址压栈,这样return语句就可以在执行完f()后返回调用f()处。这是框架的返回地址。
  • 局部变量就是f()在运行过程中使用的变量。这就是框架中的局部变量。

在整个过程中,需要返回的值是在寄存器中存储的,返回过程中,将寄存器中的数据复制出即可。然而对于非内建类型而言有时寄存器无法放下这些数据。如果将f()的返回值放在局部变量中,那么当函数返回时栈指针将指向返回地址。如果此时有其他程序的中断发生那么栈指针下的空间将分配给哪些中断程序,而此时返回值就被复盖了,故这种方式行不通。所以返回值不能存放在栈中,因为重入的原因返回值也不能放在全局变量中。

为了解决这个问题,对于含有非内建类型的函数的调用,其函数框架与前者不太相同:

....栈顶|返回值地址|...函数参数...|返回地址|...局部变量...|栈底....

若B是一个类,B = f();

  • 编译器首先要做的就是将B的地址压栈,这就是新的函数框架栈中比上一个多出的返回值地址。

    返回值不再是由寄存器复制到目的地,而是由f()直接使用栈中的返回值地址将值复制到目的地。

拷贝构造:
如果知道一定会使用按值传递来传递一个对像时,那么就需要定义拷贝构造否则就没有必要定义拷贝构造。
声名一个私有的拷贝构造函数就可以避免按值传递一个对像。 默认的拷贝构造就是位拷贝。

拷贝构造函数的定义类似于:在类中定义 HowMany(const HowMany &h);
位拷贝与初始化:

    int main()
    {
        HowMany h;
        h.PrintCounts();
        HowMany h2 = f(h);
    }

若HowMany是一个类,且其中只有默认拷贝。那么这里使用的就是按位拷贝。

当将h拷贝到函数框架中的参数表位置时不会触发类的构造函数。
但当f()执行完成之后会触发在栈中的参数表位值的对像。 故此段代码中构造函数执行类1次而析构函数执行类3次。

如果我们没有定义拷贝构造函数,那么系统会认为我们使用按位拷贝。若类中有拷贝构造函数那么由此类创建新对像时编译器会调用拷贝构造。
临时对像:
考虑到当返回非内建类型时的函数框架,如果只调用函数而不使用返回值就像 f()。因为新的函数框架中有返回值地址而这个值是在调用函数之前就压栈的,故编译器需要知道一个这样的地址。一般的情况是编译器自动生成一个临时的对像并将这个地址传递给返回值地址。

指向成员的指针:

  • 指向成员数据的指针: 一般的指针都指向一个确定的内存地址,然而指向类中成员的指针却没有指向一个实际的地址,成员指针只有相对于对像而言的偏移。故成员指针只有在和对像相结合后才能得倒实际的地址。
  • objectPointer->*pointMember;

    先读*pointMember 这表示pointMember所指向的成员,注意pointMember是指针,而*pointMember就是成员。

  • object.*pointMember;

    与上面的语句的意义相同。

成员数据指针的定义:

  • int objectclass::*pointMember = &objectclass::Member;// 虽然所成员指针没有地址,但使用&表示取偏移量。

指向成员函数的指针:

  • 定义:int (objectclass::*pointFunMember)(float )const = &objectclass::f;

    在这里没有给出f的参数表但由成员指针的参数表可以确定所要定义的成员函数。

第十二章:运算符重载 [Index]

运算符重载只是另一种函数的调用方式。

  • 其与普通的函数调用的方式的不同之处在于变量不在括号内而在运算符旁边。
  • 在仅包含内建数据类型的语句中的运算符是不能被重载的。例如:7 + 9中的 + 是不能被重载的。

运算符重载中参数的个数由两个因素决定:

  • 运算符是一元的还是二元的。
  • 运算符是全局函数的还是成员函数。
  • 説明:

    当重载的运算符作为成员函数的时侯,当前对像*this已经默认作为一个参数了。故当运算符是一元的时侯,就不再需要参数类(例如++,--),而在使用时运算符在变量的左侧。还是作为成员,当运算符是两元的时侯,我们需要为运算符重载函数提供一个参数,而这个参数默认位于运算符之右。当运算符为全局时,运算符默认有几个参数,我们就应该提供几个参数,对于正号+,我们应该提供一个参数:int &operator+(int &a)。但当使用加号+时我们应该提供两个参数:int operator+(int a , int b);

对于非条件运算符重载,如果其两侧的参数类型相同,则一般要将其返回类型也设置为与参数相同的类型,避免歧义的发生。

运算符重载注意:

  • 不能重载语言当前没有的运算符,这样会使库的使用变得困难,而运算符重载就是为了方便
  • 注意关键字this在运算符重载中的应用。

自增和自减:

  • 全局自增 ++
    • ++a 系统会调用operator++(a) 。看作一元运算符
    • a++ 调用operator++(a,int)。看作二元运算符
  • 成员自减 --
    • --a 系统会调用operator--()。一元
    • a-- operator--(int)。二元
  • 説明:

    对于自加自减运算符,在C++ 中前缀和后缀的实现方法是不同的,为了不产生歧义,在没有右值的情况下 x++ 会产生一个x的临时变量拷贝。然而对于前缀++x而言,即使没有右值,其也不会产生临时变量,这点是需要注意的。系统会自动的将临时变量设置为常量型,而我们只能调用静态常量中的静态函数成员,这点需要注意。

较为少见的运算符重载:

  • operator[]
  • operator,
  • operator->
  • << 重载这个运算符时最好使用友元形式:class {... friend ostream operator<<(ostream &os ,const current_calss); ...}

    指针间接引用运算符一定是成员函数,他有着额外的非典型的限制:他必须返回一个对像或对像的引用,而且这个对像也有一个指针间接运算符;或者返回一个指针被用于选择指针间接引用运算符箭头所指向的内容。这些限制主要可以表现在语法上。 P281

  • operator->* P284

不能重载的运算符

  • 成员运算符 .
  • 成员指针间接引用 .*
  • 自定义运算符。

成员运算符和非成员运算符 P286

  • 当运算符的左侧是是当前类的对像时,系统会调用成员运算符。

赋值号的重载

C++ 中不允许存在全局的operator=,对于一个没有创建的对像而言,使用等号会触发拷贝构造函数,然而当等号左侧的对像已经存在,那么就会调用重载的赋值号等号。 MyType b;
MyType a = b;//调用拷贝构造函数
a = b ; //使用重载的运算符等号

 

C++ 笔记(待续)

标签:

原文地址:http://www.cnblogs.com/jiahu-Blog/p/4851045.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!