C++11 Primer Plus(三)之名称空间与类

261 阅读18分钟

自己写的C++11 Primer Plus 学习笔记,如有雷同不胜荣幸,如有错误敬请指正


1. 内存模型与名称空间

1. 单独编译(程序划分)

友好的程序代码结构:

  • 头文件:包含结构声明和使用这些结构的函数的原型
  • 源代码文件:包含与结构有关的函数代码
  • 源代码问价:包含调用与结构有关的函数的代码

头文件常包含内容(不要将函数定义或变量声明放到头文件中):

  • 函数原型
  • 使用 #define 或 const 定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数

在同一个文件中只能将同一个头文件包含一次,可以在头文件中使用

#ifndef HEADER_H_
#define HEADER_H_
...
#endif

来达到目的

2. 存储持续性,作用域和链接性

C++中的不同储存方案:

  • 自动存储持续性: 在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++ 有两种储存持续性为自动的变量
  • 静态储存持续性: 在函数定义外定义的变量和使用关键字 static 定义的变量的储存持续性都为静态。它们在整个运行过程中都存在。C++ 有三种持续性为静态的变量
  • 线程储存持续性: 如果变量使用关键字 thread_local 声明的,则其声明周期与所属线程一样长
  • 动态储存持续性: 用 new 运算符分配的内存将一直存在,直到使用 delete 运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。

作用域: 描述了名称在文件打多大范围内可见
链接性: 描述了名称如何在不同单元间共享(链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量打名称没有链接性,因为它们不能共享)

① 自动存储连续性::

在默认情况下,在函数中声明的函数参数和变量差存储持续性为自动,作用域为局部,没有链接性

自动变量: 由于自动变量的数目随函数打开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法时留出一段内存,并将其视为栈

寄存器变量(register): 它建议编译器使用 CPU 寄存器来存储自动变量,这旨在提高编译器的运行速度(C++11 中,关键字 register 只是显示的指出变量是自动的)

② 静态持续变量::

  • 要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它
  • 要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用 static 限定符
  • 要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用 static 限定符

5种变量存储方式

存储描述 持续性 作用域 链接性 如何声明
自动 自动 代码块 在代码块中
寄存器 自动 代码块 在代码块中,使用关键字 register
静态,无链接性 静态 代码块 在代码块中,使用关键字 static
静态,外部链接性 静态 文件 外部 不在任何函数内
静态,内部链接性 静态 文件 内部 不在任何函数内,使用关键字 static

关于关键字static的两种用法:

  • 用于局部声明,以指出变量是无链接性的静态变量时,static 表示的是存储持续性
  • 用于代码块外的声明时,static 表示内部链接性,而变量已经是静态持续性

零初始化和常量表达式初始化统称为静态初始化,这意味着在编译器处理文件时初始化变量。**动态初始化意味着变量将在编译后初始化

③ 静态持续性,外部链接性::

链接性位外部的变量通常称为外部变量,它们的存储持续性为静态,作用域为整个文件

单定义规则: 变量只能有一次定义
C++ 提供的两种变量声明:

  • 定义声明: 它给变量分配存储空间
  • 引用声明: 它不给变量分配存储空间,因为它引用已有的变量

如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,单在使用该变量的其他所有文件中,都必须使用关键字 extern 声明它

④ 说明符和限定符::

  • auto
  • register
  • static
  • extern
  • thread_local
  • mutable

cv-限定符(const,volatile):

在默认情况下,全局变量的链接性为外部,但 const 全局变量的链接性为内部

const int fig = 5;  //same as static const int fig = 5;
  • 1

如果出于某种原因,程序员希望某个常量的链接性为外部,则可以使用 extern 关键字来覆盖默认的内部链接性:

extern const int states = 5; //definition with external linkage
  • 1

关键字 volatile 表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。该关键字的作用是为了改善编译器的优化性能。

例如:假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在两次使用之间不会变化。将变量声明为 volatile ,相当于告诉编译器,不要进行这种优化

mutable: 可以用它来指出,即使结构变量为 const ,其某个成员也可以被修改

⑤ 语言链接性与存储方案和动态分配:

C语言链接性: C 语言编译器可能将 spiff 这样的函数名翻译为 _spiff
C++ 语言链接性: C++ 编译器执行名称矫正或名称修饰,为重载函数生成不同 的符号名称。可能将 spiff(int) 转换为 _spiff_i.

可以使用函数原型来指出要使用的约定:

extern "C" void spiff(int); //use C protocol for name look-up
extern void spiff(int); //use C++ protocol for name look-up
extern "C++" void spiff(int); //use C++ protocol for name look-up

存储方案和动态分配:

编译器使用三块独立的内存:

  • 用于静态变量
  • 用于自动变量
  • 用于动态存储

new 运算符:

int *pi = new int;    <<==>>   int *pi = new sizeof(int);
int *pa = new int[40] <<==>>   int *pa = new (40 * sizeof(int));
int * pb = new int (6); //*pb set to 6
//分配函数(用于全称名称空间中)
void * operator new(std::size_t);  //use by new
void * operator new[] (std::size_t);  //use by new[]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

定位 new 运算符: 它能够指定要使用的位置。程序员可以使用这种特性来设置其内存管理规程,处理需要通过特定地址进行访问的硬件或在特定位置创建对象

#include<new>
struct chaff
{
    char dross[20];
    int slag;
};
char buffer1[20];
char buffer2[200];

int main()
{
    chaff *p1,*p2;
    int *p3,*p4;
    //first,the regular forms of new
    p1 = new chaff;        //place structure in heap
    p3 = new int[20];      //place int array in heap
    //now,the two forms of placement new
    p2 = new (buffer1) chaff;    //place structure in buffer1
    p4 = new (buffer2) int[20];  //place int array in buffer2
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上述代码从 buffer1 中分配空间给结构 chaff,从 buffer2 中分配空间给一个包含20个元素的 int 数组。

delete 只能用于这样的内存:指向常规 new 运算符分配的堆内存。

3. 名称空间

  • 声明区域: 可以在其中声明的区域
  • 潜在作用域: 变量的潜在作用域从声明点开始,到其声明区域的结尾
  • 作用域: 变量对程序而言可见的范围

名称空间可以是全局的,也可以是位于另一个名称空间中,但不能位于代码块中。因此,默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)
未被装饰的名称称为未限定的名称;包含名称空间的名称称为限定的名称

using 声明使特定的标识符可用,using 编译指令使整个名称空间可用

不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称

指导原则:

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量
  • 如果开发了一个函数库或类库,将其放到一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间 std 中
  • 仅将编译指令 using 作为一种将旧代码转换为使用名称空间的权宜之计
  • 不要在头文件中使用 using 编译指令。
  • 导入名称时,首选使用作用域解析运算符或 using 声明的方法
  • 对于 using 声明,首选将其作用域设置为局部而不是全局

2. 对象和类

1. 抽象和类

指定基本类型完成了三项工作:

  • 决定数据对象需要的内存数量
  • 决定如何解释内存中的位
  • 决定可使用数据对象执行的操作或方法

类规范:

  • 类声明: 以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有借口
  • 类方法定义: 描述如何实现类成员函数

① 访问控制

  • 关键字 private 和 public 描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数来访问对象的私有成员
  • 防止程序直接访问数据被称为数据隐藏
  • 将实现细节与抽象分开被称为封装

② 实现类成员函数

  • 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类
  • 类方法可以访问类的 private 组件

内联函数: 定义位于类声明中的函数都将自动成为内联函数
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义

2. 类的构造函数和析构函数

防止类成员名称用作构造函数的参数名:

  • 在数据成员中使用 _m 前缀
  • 在数据成员中使用 _ 后缀

① 使用构造函数

  • 显式的调用构造函数:Stock food = Stock("a",12);
  • 隐式的调用构造函数:Stock garment("b",21);
  • 与 new 一起使用:Stock *pstock = new Stock("c",212);

②默认构造函数
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数:Stock flu;
(当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数)
默认构造函数:Stock();

在设计类时,通常应提供对所有成员函数做隐式初始化的默认构造函数

Stock first("a",12);        //calls constructor
Stock second();             //declares a function
Stock third;                //calls default constructor
  • 1
  • 2
  • 3

第一个声明调用非默认构造函数,即接受参数的构造函数
第二个声明指出,second() 是一个返回 Stock 对象的函数
第三个隐式的调用默认构造函数,不使用圆括号

③ 析构函数
析构函数在对象过期时由程序自动调用来释放对象,析构函数没有参数且类名前加 ,如: ~stock()。通常不应在代码中显式的调用析构函数

  • 如果创建是静态存储类对象,则其析构函数将在程序结束时自动被调用
  • 如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时自动被调用
  • 如果对象是通过 new 创建的,则它将驻留在栈内存或自由存储区中,当使用 delete 来释放内存时,其析构函数将自动被调用
  • 如果程序创建临时对象来完成特定的操作,则程序将在结束对该对象的使用时自动调用其析构函数

④ 对象初始化:

  • Stock stock1("a",12,2.0); //short form 程序创建一个 stock1 对象并初始化
  • Stock stock2 = Stock("a",12,2.0); //primary form
    该方法的执行方式由两种:
    ①. 程序创建一个 stock2 对象并初始化
    ②. 允许构造函数创建一个临时对象,然后将该临时对象复制到 stock2 中,并丢 弃它。如果编译器使用这种方式,则将为临时对象调用析构函数
  • Stock *stock3 = new Stock("a",12,2.0); //dynamic object
  • stock3 = stock1;stock3 = Stock("a",12,2.0); 这两条语句是赋值(stock3 对象已经存在)

列表初始化(C++11):
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序

  • Stock hot1 = {"a",12,2.0};
  • Stock hot2 {"a",12,2.0};
  • Stock *hot3 = new Stock{"a",12,2.0};

如果构造函数只有一个参数:

  • Bozo bozo1 = Bozo(44);
  • Bozo bozo2(55);
  • Bozo bozo3 = 66;

⑤ const 成员函数
void show() const; 方式声明和定义的类函数为const 成员函数。
只要类方法不修改调用对象,就应将其声明为 const。

3. this 指针: 指向用来调用成员函数的对象(this 被作为隐藏参数传递给方法)

this 是对象的地址,而 *this 是对象本身

对于 const Stock & topval(const Stock & s) const;,① 括号中的 const 表明,该函数不会修改被显示的访问对象;② 而括号后的 const 表明,该函数不会修改被隐式的访问的对象;③ 由于该函数返回了两个 const 对象之一的引用,因此返回类型也应为 const 引用。(该函数隐式的访问一个对象,而显示的访问另一个对象,并返回其中一个对象的引用)

对于 top = stock1.topval(stock2);,该格式隐式的访问 stock1,而显示的访问 stock2。

每个成员函数(包括构造函数和析构函数)都有一个 this 指针。this 指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式 *this。在函数的括号后面使用 const 限定符将 this 限定为 const ,这样将不能使用 this 来修改对象的值。

4. 类作用域

声明类只是描述了对象的形式,并没有床架对象

在类中定义的名称(如类数据成员和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。

enum class egg {small,medium,large}; 
enum class : short piz {small,medium,large};  //将底层类型指明为 short
  • 1
  • 2

3. 使用类

1. 运算符重载

要重载运算符,要使用被称为运算符函数的特殊函数形式 operator op(argument-list)。例如:operator +() 重载 + 运算符,operator *() 重载 * 运算符。(op 必须是有效的C++运算符)

class Time
{
private:
    int hours;
    int minutes;
public:
    Time operator+(const Time & t) const;
}
////////////////////////
Time Time::operator+(cinst Time & t) const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

调用:

total = coding.operator+(fixing);   //function notation
total = coding + fixing;            //operator notation
  • 1
  • 2

2. 重载限制

① 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将减法运算符(-)重载为计算两个 double 值的和,而不是它们的差。虽然这种限制将对创造性有所影响,但可以确保程序正常运行。

② 使用运算符时不能违反运算符原来的句法规则,同样,不能修改运算符的优先级。
③ 不能创建新运算符
④ 不能重载下面的运算符:

  • sizeof:sizeof 运算符
  • .:成员运算符
  • .* :成员指针运算符
  • :::作用域解析运算符
  • ?::条件运算符
  • typeid:一个RTTI运算符
  • const_cast:强制类型转换运算符
  • dynamic_cast:强制类型转换运算符
  • reinterpret_cast:强制类型转换运算符
  • static_cast:强制类型转换运算符

⑤ 下面运算符只能通过成员函数进行重载

  • =:赋值运算符
  • ():函数调用运算符
  • []:下表运算符
  • ->:通过指针访问类成员的运算符

可重载的运算符

+ - * / % ^
& | ~= ! = <
> += -= *= /= %=
^= &= |= << >> >>=
<<= == != <= >= &&
|| ++ , ->* ->
0 [] new delete new[] delete[]

3. 友元 (友元函数,友元类,友元成员函数)

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。即,友元是一类特特殊的非成员函数可以访问类的私有成员

friend Time operator*(double m,const Time & t);
该原型意味着:

  • 虽然 operator*() 函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
  • 虽然 operator*() 函数不是成员函数,但它与成员函数的访问权限相同
  • 另外,它不是成员函数,所以不要使用 Time:: 限定符,同时不要在定义中使用关键字 friend

重载 << 运算符::
法一:

void operator<<(ostream & os,const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
}
  • 1
  • 2
  • 3
  • 4

调用语句:cout<<trip
按下面这样的格式打印数据:4 hours, 23 minutes

法二:

ostream & operator<<(ostream & os,const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os;
}
  • 1
  • 2
  • 3
  • 4
  • 5

调用语句:operator<<(cout,trip);

加法运算符需要两个操作符。对于成员函数版本来说,一个操作数通过 this指针 隐式的传递,另一个操作数作为函数参数显示的传递;对于友元版本来说,两个操作数都作为参数来传递。
注意: 非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式的传递的调用对象。

T1 = T2.operator+(T3); //member function
T1 = operator+(T2,T3); //nomember function

4. 类的自动转换和强制类型转换

构造函数可以隐式自动类型转换函数,但关键字explicit 用于关闭隐式自动转换,但可以显示转换。如:explicit Stock(double lbs);

编译器可用于下面的隐式转换:

  • 将 Stock 对象初始化为 double 值时
  • 将 double 值赋给 Stock 对象时
  • 将 double 值传递给接受 Stock 参数的函数时
  • 返回值被声明为 Stock 的函数试图返回 double 值时
  • 在上述任意一种情况下,使用可转换为 double 类型的内置类型时

创建转换函数: operator typeName();(typeName 为要转换的类型)
注意: ① 转换函数必须是类方法 ② 转换函数不能制定返回类型 ③ 转换函数不能有参数

typeName 指出了要转换成的类型,因此不需要返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。(应谨慎使用隐式转换函数,最好选择显示转换函数)

消除隐式转换:
① 使用 explicit 关键字
② 用一个功能相同的非转换函数替换该转换函数即可,即仅能被显示调用
可以将Stock::operator int(){return int(pounds + 0.5);} 转换为 int Stock::Stone_to_Int() {return int(pounds + 0.5);}。这样下面的语句int plb = poppins 将是非法的,正确方法:int plb = poppins.Stone_to_Int();(poppins 为 Stock 对象)

C++ 提供的类型转换:

  • 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如:将 int 值赋给 Stock 对象时,接受 int 参数的 Stock 类构造函数自动调用。然而,在构造函数声明中使用 explicit 可防止隐式转换,而只允许显示转换。
  • 被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员,没有返回类型,没有参数,名为 operator typeName(),其中,typeName 是对象被转换成的类型。将类对象赋给 typeName 变量或将其强制转换为 typeName 类型时,该转换函数将自动被调用。