关闭

总结C++ Primer中的notes(一)

964人阅读 评论(0) 收藏 举报

说明:

C++ Primer, Fourth Edition (中英文)下载地址:http://download.csdn.net/detail/ace_fei/4165568

以下内容截取自该书籍,都是一些基础而又容易忽略的知识点


初窥输入/输出

endl 是一个特殊值,称为操纵符,将它写入输出流时,具有输出换行的效果,并刷新与设备相关联的缓冲区。通过刷新缓冲区,用户可立即看到写入到流中的输出。

比如下面这段程序可以看出,如果没有cout << endl;刷新缓冲区, 那么要等10秒后,程序结束时,才能打印出字符串。

#include <iostream>
using namespace std;
int main()
{
    cout <<"Sleep!";
//  cout << endl;
    sleep(10);
    return 0;
} 

程序员经常在调试过程中插入输出语句,这些语句都应该刷新输出流。忘记刷新输出流可能会造成输出停留在缓冲区中,如果程序崩溃,将会导致程序错误推断崩溃位置。


使用内置算术类型

对于 unsigned 类型来说,负数总是超出其取值范围。unsigned 类型的对象可能永远不会保存负数。有些语言中将负数赋给 unsigned 类型是非法的,但在 C++ 中这是合法的。

C++ 中,把负值赋给 unsigned 对象是完全合法的,其结果是该负数对该类型的取值个数求模后的值。所以,如果把 -1 赋给8位的 unsignedchar,那么结果是 255,因为 255 是 -1 对 256 求模后的值。

若在某机器上short 类型占16 位,那么可以赋给short类型的最大数是2的15次方-1,即32767;而unsignedshort 类型的最大数为2的16次方-1,即65535。

当将超过取值范围的值赋给 signed 类型时,由编译器决定实际赋的值。在实际操作中,很多的编译器处理signed 类型的方式和 unsigned 类型类似。也就是说,赋值时是取该值对该类型取值数目求模后的值。然而我们不能保证编译器都会这样处理 signed 类型。

如果您要处理的只是非负整数,那么应该优先使用unsigned 打头的那些整数类型。如果您要处理的整数超出了int所能表示的范围,并且您的编译器中,long的表示范围比int大,那就使用long。不过,若非必要,尽量不要用long,因为它可能会降低程序运行效率。有一点要注意:如果您的编译器中,long和int都是32位的,并且您需要使用32位整数,那么应该用long,而不要用int。只有这样,我们的程序才可以安全地移植到16位的计算机,因为16位的计算机,int一般也是16位的。类似地,如果您需要使用64位整数,那就用long long。如果int是32位的话,那么使用short可以节省空间,不过您得确保您要处理的整数不会超出short的表示范围。这种“节省”对内存大的计算机来说,是没什么意义的。

决定使用哪种浮点型进行运算:使用 double 类型基本上不会有错。 在 float 类型中隐式的精度损失是不能忽视的,而 double 类型精度代价相对于 float 类型精度代价可以忽略。事实上,有些机器上,double 类型比 float 类型的计算要快得多。long double 类型提供的精度通常没有必要,而且还需要承担额外的运行代价。


建议:不要依赖未定义行为

使用了未定义行为的程序都是错误的,即使程序能够运行,也只是巧合。未定义行为源于编译器不能检测到的程序错误或太麻烦以至无法检测的错误。

不幸的是,含有未定义行为的程序在有些环境或编译器中可以正确执行,但并不能保证同一程序在不同编译器中甚至在当前编译器的后继版本中会继续正确运行,也不能保证程序在一组输入上可以正确运行且在另一组输入上也能够正确运行。

程 序不应该依赖未定义行为。同样地,通常程序不应该依赖机器相关的行为,比如假定 int 的位数是个固定且已知的值。我们称这样的程序是不可移植的。当程序移植到另一台机器上时,要寻找并更改任何依赖机器相关操作的代码。在本来可以运行的程序中寻找这类问题是一项非常不愉快的任务。


关键概念:强静态类型

C++ 是一门静态类型语言,在编译时会作类型检查。一些程序设计语言,特别是 Smalltalk 和 Python,在运行时才检查语句中对象的类型。

在大多数语言中,对象的类型限制了对象可以执行的操作。如果某种类型不支持某种操作,那么这种类型的对象也就不能执行该操作。

在 C++ 中,操作是否合法是在编译时检查的。当编写表达式时,编译器检查表达式中的对象是否按该对象的类型定义的使用方式使用。如果不是的话,那么编译器会提示错误,而不产生可执行文件。

随着程序和使用的类型变得越来越复杂,我们将看到静态类型检查能帮助我们更早地发现错误。静态类型检查使得编译器必须能识别程序中的每个实体的类型。因此,程序中使用变量前必须先定义变量的类型( 写惯perl 、shell、python这些脚本的人需要注意 )。


什么是变量

变量提供了程序可以操作的有名字的存储区。C++中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储于该内存中的值的取值范围以及可应用在该变量上的操作集。C++ 程序员常常把变量称为“变量”或“对象(object)”


术语:什么是对象?

C++ 程序员经常随意地使用术语对象。一般而言,对象就是内存中具有类型的区域。说得更具体一些,计算左值表达式就会产生对象。

严格地说,有些人只把术语对象用于描述变量或类类型的值。有些人还区别有名字的对象和没名字的对象,当谈到有名字的对象时一般指变量。还有一些人区分对象和值,用术语对象描述可被程序改变的数据,用术语值描述只读数据。

在本书中,我们遵循更为通用的用法,即对象是内存中具有类型的区域。我们可以自由地使用对象描述程序中可操作的大部分数据,而不管这些数据是内置类型还是类类型(也就是说int i;这里的i也称之为对象),是有名字的还是没名字的,是可读的还是可写的。


初始化

C++ 支持两种初始化变量的形式:复制初始化直接初始化。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:

      intival(1024);     // direct-initialization
      int ival =1024;    // copy-initialization

这里,理解“初始化不是赋值”是必要的。初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替记住:当初始化类类型对象时,直接初始化语法更灵活且效率更高。对内置类型来说,复制初始化和直接初始化几乎没有差别。


变量初始化规则

内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成 0(全局变量和static变量都会被自动初始化为0),在函数体里定义的内置类型变量不进行自动初始化(编译器一般会分配给它一个随机值)。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难于发现。正如我们在之前劝告的,永远不要依赖未定义行为。


警告:未初始化的变量引起运行问题

使用未初始化的变量是常见的程序错误,通常也是难以发现的错误。虽然许多编译器都至少会提醒不要使用未初始化变量,但是编译器并未被要求去检测未初始化变量的使用。而且,没有一个编译器能检测出所有未初始化变量的使用。

有时我们很幸运,使用未初始化的变量导致程序在运行时突然崩溃。一旦跟踪到程序崩溃的位置,就可以轻易地发现没有正确地初始化变量。

但有时,程序运行完毕却产生错误的结果。更糟糕的是,程序运行在一部机器上时能产生正确的结果,但在另外一部机器上却不能得到正确的结果。添加代码到程序的一些不相关的位置,会导致我们认为是正确的程序产生错误的结果。

问题出在未初始化的变量事实上都有一个值。编译器把该变量放到内存中的某个位置,而把这个位置的无论哪种位模式都当成是变量初始的状态。当被解释成整型值时,任何位模式(位模式:计算机中所有二进制的0、1代码所组成的数字串。)都是合法的值——虽然这个值不可能是程序员想要的。因为这个值合法,所以使用它也不可能会导致程序崩溃。可能的结果是导致程序错误执行和/或错误计算。(条款04 :确定对象被使用前已先被初始化)


声明和定义

变量的定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。

声明用于向程序表明变量的类型和名字定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:

      extern int i;   // declares but does not define i
      int i;          // declares and defines i
extern 声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次

只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为 extern:

      extern double pi =3.1416; // definition

虽然使用了 extern ,但是这条语句还是定义了 pi,分配并初始化了存储空间。只有当 extern 声明位于函数外部时,才可以含有初始化式。

因为已初始化的 extern 声明被当作是定义,所以该变量任何随后的定义都是错误的:

      extern double pi =3.1416; // definition
      double pi;                 // error: redefinition of pi

同样,随后的含有初始化式的 extern 声明也是错误的:

      extern double pi =3.1416; // definition
      extern doublepi;          // ok: declaration notdefinition
      extern double pi =3.1416; // error: redefinition of pi

任何在多个文件中使用的变量都需要有与定义分离的声明。在这种情况下,一个文件含有变量的定义使用该变量的其他文件则包含该变量的声明(而不是定义)。


在变量使用处定义变量

一般来说,变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。

Best practice: 通常把一个对象定义在它首次使用的地方是一个很好的办法。(条款26:尽可能延后变量定义式的出现时间)

 

定义 const 对象

因为常量在定义后就不能被修改,所以定义时必须初始化。

 

const 对象默认为文件的局部变量

在全局作用域里定义非 const 变量时,它在整个程序中都可以访问。我们可以把一个非 const 变更定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:

      // file_1.cc
      int counter;  // definition (非 const 变量默认为extern)
      // file_2.cc
      extern intcounter; // uses counter from file_1
      ++counter;          // increments counter defined infile_1

与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量这样设置默认情况的原因在于允许const 变量定义在头文件中)。此变量只存在于那个文件中,不能被其他文件访问。

通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象

      // file_1.cc
      // defines andinitializes a const that is accessible to other files
      externconst int bufSize = fcn();
      // file_2.cc
      externconst int bufSize; // uses bufSize from file_1
      // uses bufSizedefined in file_1
      for (int index =0; index != bufSize; ++index)

Note:非 const 变量默认为extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern。

引用

引用就是对象的别名。在实际程序中,引用主要用作函数的形式参数。

引用是一种复合类型(另外两种复合类型:指针和数组),通过在变量名前添加“&”符号来定义。复合类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都“关联到”某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。

引用必须用与该引用同类型的对象初始化:(区别于:const 引用)

      int ival = 1024;
      int &refVal =ival; // ok: refVal refers to ival
      int&refVal2;       // error: a referencemust be initialized
      int &refVal3 =10;  // error: initializer must be anobject
Note:当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。

(条款21:必须返回对象时,别妄想返回其reference)


const 引用

Note:非 const 引用只能绑定到与该引用同类型的对象。
             const 引用则可以绑定到不同但相关的类型的对象或绑定到右值。

const 引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:

      int i = 42;
      //  legal for const references only
      const int &r =42;
      const int &r2= r + i;
      double dval =3.14;
      const int &ri= dval;



typedef 名字

typedef 通常被用于以下三种目的:

为了隐藏特定类型的实现,强调使用类型的目的。

简化复杂的类型定义,使其更易理解。

允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。


枚举成员是常量

可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式。常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的const 对象也是常量表达式一样。

枚举成员值可以是不唯一的。

枚举类型的对象的初始化或赋值,只能通过其枚举成员同一枚举类型的其他对象来进行。

#include<iostream>
#define x 1  
const int y = 2; //constant expression
enum Points
{
        point1 = x,
        point2 = y,
        point3,
        point4 = 3,
};

int main()
{
        Points p1 = point3; //  ok: point3d is a Points enumerator
        //Points pt2w = 3;       // error: pt2w initialized with int
        Points p2; 
        //pt2w = polygon;        // error: polygon is not a Points enumerator
        p2 = p1;    // ok: both are objects of Points enum type
        return 0;
}

类类型

每个类都定义了一个接口和一个实现。接口由使用该类的代码需要执行的操作组成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。

编程新手经常会忘记类定义后面的分号,这是个很普遍的错误!

用 class 和struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而class 的成员为 private


头文件用于声明而不是用于定义

头文件一般包含类的定义、extern 变量的声明和函数的声明。对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的const 对象(即const变量是用常量表达式初始化)和inline 函数。

设计头文件不能太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。

因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。(因为程序中定义只能出现一次,如果含有定义头文件包含在多个源文件之中,就会出现重复定义)

 

一些 const 对象定义在头文件中

如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。

当我们在头文件中定义了 const 变量后,每个包含该头文件的源文件都有了自己的 const 变量,其名称和值都一样。当该 const 变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的const 变量

以下是来自网上的一段代码,解释的很好:

#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。

 

避免多重包含

头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。

在编写头文件之前,我们需要引入一些额外的预处理器设施。预处理器允许我们自定义变量。预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。#define 指示接受一个名字并定义该名字为预处理器变量。#ifndef 指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现#endif。

可以使用这些设施来预防多次包含同一头文件:

     #ifndef SALESITEM_H
     #define SALESITEM_H
     // Definition of Sales_itemclass and related functions goes here
    #endif






 

 


1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:435508次
    • 积分:5445
    • 等级:
    • 排名:第4895名
    • 原创:123篇
    • 转载:61篇
    • 译文:3篇
    • 评论:30条
    文章分类
    最新评论