现在的位置: 首页 > 综合 > 正文

C++ Primer学习笔记——$7 语句和函数

2018年03月30日 ⁄ 综合 ⁄ 共 5107字 ⁄ 字号 评论关闭
题记:本系列学习笔记(C++ Primer学习笔记)主要目的是讨论一些容易被大家忽略或者容易形成错误认识的内容。只适合于有了一定的C++基础的读者(至少学完一本C++教程)。
 
作者: tyc611, 2007-01-20

   本文主要讨论C++中各种语句及函数中的一些话题。
   如果文中有错误或遗漏之处,敬请指出,谢谢!


语句
   在控制语句中可以定义变量,特别是在条件表达式中,该表达式的值就是变量的值。例如:
 

   while (int i = getNum())
      cout<<"While: "<<i<<endl;

   for (int i = 0; i < 10; ++i)
      cout<<"For: "<<i<<endl;

   if (int i = getNum())
      cout<<"If: "<<i<<endl;
   else
      cout<<"If: 0"<<endl;

   switch(int i = getNum())
   {
   case 1:
   case 2:
      cout<<"Switch: 1 or 2? "<<i<<endl;
      break;
   default:
      cout<<"Switch: others? "<<i<<endl;
   }

   在上面例子,要特别注意while循环中定义的变量和for循环中定义的变量的生存期的区别。
   另外,在switch语句中,case标号必须是整型常量表达式。若要switch语句内定义变量,只能在最后一个case标号或者default标号后面定义变量。这个规则主要是为了避免出现代码跳过变量的定义和初始化的情况。当需要在某个caes语句中定义变量时,可以使用块语句,然后在这块语句中定义变量。
 
try-throw-catch 语句
   当抛出一个异常时,首先要搜索的是抛出异常的函数,如果没有找到匹配的catch子句,则终止这个函数的执行。然后在调用这个函数的函数中继续寻找相匹配的catch。如果仍然没有找到,该函数终止,继续向上,直到找到相匹配的catch。如果不存在处理该异常的catch,程序的运行就要跳转到名为 terminate的标准库函数(头文件<exception>)。该函数的行为依赖于系统,通常它的行为将导致程序非正常退出。
 
标准异常
   C++标准库异常类定义在四个头文件中:
   1) <exception>头文件中定义了异常类exception;
   2) <stdexcept>头文件中定义了几种常见的异常类。
   3) <new>头文件中定义了bad-alloc异常类。当new无法分配内存时将抛出该异常类对象。
   4) <type_info>头文件中定义了bad_cast异常类。当dynamic_cast失败时将抛出该异常类对象。
   标准异常类之间的关系:exception派生出runtime_error类和logic_error类。由runtime_error派生出range_error、overflow_error、underflow_error;由logic_error派生出domain_error、invalid_argument、length_error、out_of_range。
 
标准异常类的详细列表
 exception  最常见的问题
 runtime_error  运行时错误:仅在运行时才能检测到的问题
 range_error  运行时错误:生成的结果超出了有意义的值域范围
 overflow_error  运行时错误:计算上溢
 underflow_error  运行时错误:计算下溢
 logic_error  逻辑错误:可在运行前检测到的问题
 domain_error  逻辑错误:域错误
 invalid_argument  逻辑错误:无效参数
 length_error  逻辑错误:试图生成一个超出该类型最大长度的对象
 out_of_range  逻辑错误:使用一个超出有效范围的值
注:运行时错误是指在某语句计算过程中产生的错误,逻辑错误是指在某语句执行前检查到的错误。
 
   exception、bad_alloc、bad_cast类型只定义了默认构造函数,而其它类型则只定义了一个使用string作为参数的构造函数。基类exception提供了一个what()成员函数,其返回const char*类型的C风格字符串。对于以string初始化的异常类,what()将返回该string对应的C风格字符串;否则返回的值是未定义的。
 
预处理
几个预处理常量
   1) __FILE__ 文件名
   2) __LINE__ 当前行号
   3) __TIME__ 文件被编译的时间
   3) __DATE__ 文件被编译的日期
 
assert宏
   assert宏定义在头文件<cassert>中,其使用语法形如:assert(expr);
其中,当expr结果为0时,assert输出信息并且终止程序的执行;否则,assert不做任何操作。


函数
   标准C++的函数必须指定函数返回类型。
   如果main函数最后一个语句不是返回语句,则编译器会隐式地插入返回0的语句。<cstdlib>定义了两个预处理变量:EXIT_FAILURE和EXIT_SUCCESS,分别代表程序运行失败和成功。
 
数组作为引用参数

   形参就是把数组定义中的数组名用 (& ref)的形式替换即可,此时必须指定数组大小。例如:
      int fun(int (&arr)[10]);
注意:(& ref)两边的圆括号不能少。否则,int &arr[10]表示有10个引用元素的数组(注意:C++中不能定义元素类型为引用的数组)。

 注:数组元素类型不能为引用、void、函数类型或者抽象类类型。
 
含有可变形参的函数
   在无法列举出传递给函数的所有实参的类型的数目时,可以作用省略符形参。省略符形参暂停了类型检查机制。当调用函数时,可以有0或者多个实参,而实参的类型未知。有两种形式:
   void foo(parM_list, ...);
   void foo(...);
第一种形式为特定数目的形参提供了声明。在这种情况下,当函数被调用时,对于与显示声明的形参相对应的实参进行类型检查,而对于与省略号对应的实参则暂停类型检查。在第一种形式中,形参后面的逗号是可选的。
 
默认参数

   默认实参的初始化式可以是任何适当类型的表达式,且能够在使用默认参数时能在编译时计算出该表达式的值。比如,可以是函数调用,只要该函数调用能成功计算机适当类型的值。例如:

 

int getNum(int num){
 int sum = 1;
 for (int i =1; i <= num; ++i)
  sum = sum * i;
 return sum;
}
int foo(int a, int b = getNum(5))
{
 return a+b;
}
int main()
{
    cout<<foo(1)<<endl;
    return 0;
}

注意:既可以在函数声明也可以在函数定义中指定默认参数。但是,在一个文件中,只能为一个形参指定默认参数一次。如果在函数定义的形参表中提供默认参数,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才是有效的。
   所以,我们应在函数声明中指定默认参数,并将其放在头文件中,然后在使用该函数时通过包含该头文件就可以得到默认参数。
 
内联函数(inline)

 

   内联指示符(inline specification)对于编译器来说只是一个建议,编译器可以选择展开或者忽略它。大多数编译器不支持递归函数的内联。
   内联函数应该在头文件中定义,因为内联函数的定义对编译器必须是可见的,以便编译器能够在调用点内联展开该函数的代码。
 
类的成员函数
 
   编译器隐式地将类内定义的成员函数当作内联函数。
   每个非静态成员函数都有一个额外的、隐含的形参 this。
 
const成员函数
   在定义和声明类成员函数时,可以在形参表后面用const修饰函数,表明这是一个const成员函数。const成员函数相当于把this指针指向的对象修饰成了const对象,所以不能在const成员函数中对this指向的对象进行修改。
 注:const对象只能调用const成员函数,这也是const成员函数存在的理由。
 
构造函数的初始化列表(constructor initializer list)

   构造函数的初始化列表跟在形参表之后,以冒号开头,然后是一系列成员名,每个成员后面是括在圆括号内的初始值。多个成员的初始化用逗号分隔。初始化列表中的变量表示在构造变量时进行初始化。例如:

 

class demo
{
public:
   demo(const string& name, int num): m_name(name), m_num(num){}
private:
   string m_name;
   int m_num;
};

   为什么要引入初始化列表呢?
   有两个原因:其一,当有const、引用等成员变量时,或者对于类类型成员变量来说,如果该成员所对应类没有默认构造函数,那么此时必须在定义时进行初始化,此时就必须用初始化列表;其二,如果在初始化列表中初始化类类型成员变量,这是直接调用相应构造函数,而在构造函数体中初始化是先调用默认构造函数,再调用赋值运算符,所以此时在初始化列表中初始化可以提高效率。
 
重载函数(overloaded function)

 

实参类型转换
   为了确定最佳匹配,编译器将实参类型到相应形参类型的转换划分为四个等级,优先顺序如下:
   1) 精确匹配:实参与形参类型相同;
   2) 通过类型提升实现的匹配;
   3) 通过标准转换实现的匹配;
   4) 通过类类型转换实现的匹配。
   仅当形参是引用或指针时,形参有const和无const的才是重载的。例如:
      void foo(const string);
      void foo(string);
这两个函数不是重载函数,是重复声明。因为不论哪一个函数中形参都不会影响实参。
 
函数指针

 

   在引用函数名但又没有调用该函数时,函数名将被自动解释为指向函数的指针。此时,直接使用函数名和在函数名前加上取地址符(&)是等效的。
   函数指针只能通过同类型的函数、函数指针或者0值常量表达式来初始化或赋值。
   使用函数指针调用它所指向的函数时,可以把指针当作函数名那样调用pf(...),也可以用指针的解引用形式调用(* pf)(...),效果一样。
   函数指针形参有两种形式:一是声明为函数类型,此时将自动转换为函数指针;二是显示声明为函数指针。例如:
      // two equivalent declarations
      void foo(void (int));     // foo has a parameter of function type
      void foo(void (*)(int));  // foo has a parameter of a pointer to function
   函数可以返回函数指针。
 
   阅读有函数指针的最佳方法是从声明的名字开始由里而外理解。当然最好用typedef将函数指针定义为一个别名,这样要清晰很多。例如:
      void (*foo(int))(int);
foo是一个参数为int类型并且返回值为void (*)(int)类型的函数指针的函数。如果用typedef要清晰很多,如前面的声明等价于下面的声明:
      typedef void (* PF)(int);
      PF foo(int);
   允许将函数形参定义为函数类型,但不允许函数的返回类型为函数类型,此时只能返回函数指针。例如:
      typedef void func(int);
      void f1(func);    // ok: f1 has a parameter of function type
      func* f3(int);    // ok: f3 has returns a pointer to function type
      func f2(int);     // error: f2 has a return type of function type
   对于指向重载函数的指针,指针的类型必须与重载函数的一个版本精确匹配。
 

   如果文中有错误或遗漏之处,敬请指出,谢谢!

参考文献:
[1] C++ Primer(Edition 4)
[2] Thinking in C++(Volume Two, Edition 2)
[3] International Standard:ISO/IEC 14882:1998

抱歉!评论已关闭.