第1章 绪论

面向对象的程序设计方法:

  • 由面向对象的高级语言支持
  • 一个系统由对象构成
  • 对象与对象之间通过消息进行通信

面向对象支持:

抽象与分类;封装;继承;多态


第2章 C++简单程序设计

常量

在程序运行的整个过程中其值始终不可改变的量;

直接使用符号或文字表示的值;

文字常量

整数常量12,浮点数常量3.5,字符常量’A’,C风格字符串常量"China"(末尾为\0),C++风格则为string对象

符号常量

const 数据类型说明符 常量名=常量值;

或:

数据类型说明符 const 常量名=常量值;

例如,可以定义一个代表圆周率的符号常量:

const float PI = 3.1415926;

符号常量在定义时一定要初始化,在程序中间不能改变其值

变量

在程序的运行过程中,其值可变的量

变量定义

数据类型 变量名1, 变量名2, …, 变量名n;

变量的声明与定义分离

变量能且仅能被定义一次,但是可以被多次声明。

为了支持分离式编译,C++将定义和声明区分开。其中声明规定了变量的类型和名字定义除此功能外还会申请存储空间并可能为变量赋一个初始值。

extern
如果想声明一个变量而非定义它,就使用关键字extern并且不要显式地初始化变量:

extern int i;      // 声明i而非定义i
extern int i = 1;  // 定义i, 这样做抵消了extern的作用

变量的存储类型

  • auto存储类型:采用堆栈方式分配内存空间,属于暂时性存储,其存储空间可以被若干变量多次覆盖使用。

  • register存储类型:存放在通用寄存器中。

  • extern存储类型:在所有函数和程序段中都可引用。

  • static存储类型:在内存中是以固定地址存放的,在整个程序运行期间都有效。

初始化

C++语言中提供了多种初始化方式;

例如:

int a = 0;int a(0);int a = {0};int a{0};

其中使用大括号的初始化方式称为列表初始化,列表初始化时不允许信息的丢失。例如用double值初始化int变量,就会造成数据丢失。

赋值运算

对于赋值表达式:如n=n+5

  • 表达式的值
    赋值运算符左边对象被赋值后的值

  • 表达式的类型
    赋值运算符左边对象的类型

逗号运算和逗号表达式

  • 格式

表达式1,表达式2

  • 求解顺序及结果
  1. 先求解表达式1,再求解表达式2
  2. 最终结果为表达式2的值

例:

a = 3 * 5 , a * 4 最终结果为60

"||"的运算规则

表达式1 || 表达式2

  • 先求解表达式1
  • 若表达式1的值为true,则最终结果为true,不再求解表达式2

若表达式1的结果为false,则求解表达式2,以表达式2的结果作为最终结果
注意“&&”和“‖”运算符具有“短路”特性,即若第一个条件就能判断整个逻辑语句的结果,就不继续后续的条件判断了

sizeof运算

sizeof (类型名)
或 sizeof 表达式

结果值:
“类型名”所指定的类型,或“表达式”的结果类型所占的字节数。

例:

sizeof(short)

sizeof x //其中x为变量

位运算

按位与(&)

用途:

  • 将某一位置0,其他位不变。
    例如:将char型变量a的最低位置0: a = a & 0xfe; ;(0xfe:1111 1110)

  • 取指定位。
    例如:有char c; int a; 取出a的低字节,置于c中:c=a & 0xff; (0xff:1111 1111)

按位或(|)

用途:

将某些位置1,其他位不变。
例如:将 int 型变量 a 的低字节置 1 : a = a | 0xff;

按位异或(^)

若对应位相同,则结果该位为 0,
若对应位不同,则结果该位为 1。

用途举例:使特定位翻转(与0异或保持原值,与1异或取反)

例如:要使 01111010 低四位翻转:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IPsPZNa1-1644288240037)(C:\Users\HJL\AppData\Roaming\Typora\typora-user-images\image-20220111175929183.png)]

移位(<<、>>)

  • 左移运算(<<)

左移后,低位补0,高位舍弃。

  • 右移运算(>>)

右移后:

低位:舍弃

高位:分为算术右移与逻辑右移

无符号数:补0

有符号数:补“符号位” 0表示正,1表示负

混合运算时数据类型的转换——显式转换

显式类型转换的作用是将表达式的结果类型转换为类型说明符所指定的类型。

语法形式

  • 类型说明符(表达式)
  • (类型说明符)表达式
  • 类型转换操作符<类型说明符>(表达式)
  • 类型转换操作符可以是:
    const_cast、dynamic_cast、reinterpret_cast、static_cast

例:int(z), (int)z, static_cast(z) 三种完全等价

自定义类型

类型别名

  • typedef 已有类型名 新类型名表

例:

typedef double Area, Volume;typedef int Natural;Natural i1,i2;Area a;Volume v;
  • using 新类型名 = 已有类型名;

例:

using Area = double;using Volume = double;

不限定作用域枚举类型

  • 枚举元素是常量,不能对它们赋值

例如有如下定义
enum Weekday {SUN, MON, TUE, WED, THU, FRI, SAT};

不能写赋值表达式:SUN = 0

  • 枚举元素具有默认值,它们依次为: 0,1,2,…。

  • 也可以在声明时另行指定枚举元素的值,如:

enum Weekday{SUN=7,MON=1,TUE,WED, THU,FRI,SAT};

  • 也可以在声明时另行指定枚举元素的值;
  • 枚举值可以进行关系运算。
  • 整数值不能直接赋给枚举变量,如需要将整数赋值给枚举变量,应进行强制类型转换
  • 枚举值可以赋给整型变量。

类中定义枚举类型

class datatype{enum{character, integer, floating_point} vartype; union{char c; int i; float f;};
public: datatype(char ch) {vartype=character;c=ch;}datatype(int ii) {vartype=integer; i=ii;}datatype(float ff) {vartype=floating_point; f=ff;}void print();
}void datatype::print){switch(vartype) {case character:cout << "字符型:"<<c<<endl;break;case integer:cout << "整型:"<<i<<endl;break;case floating_point:cout<<"浮点型:"<<f<<endl;break;}
}

auto类型与decltype类型

  • auto:编译器通过初始值自动推断变量的类型

例如:auto val = val1 + val2;

如果val1+val2是int类型,则val是int类型;

如果val1+val2是double类型,则val是double类型。

  • decltype:定义一个变量与某一表达式的类型相同,但并不用该表达式初始化变量

例如:decltype(i) j = 2; //j的类型与i相同


第3章 函数

值传递:单向传递。

引用传递:双向传递。 对于不需要双向传递的情况下,采用引用传递是为了节省时间与空间的开销。对于基本类型来讲不明显,但是对于对象作为参数传递效果很明显。

含有可变参数的函数

如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型

initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,该类型定义在initializer_list的头文件中

initializer_list的使用方法:

  • initializer_list是一个类模板(第9章详细介绍模板)

  • 使用模板时,我们需要在模板名字后面跟一对尖括号,括号内给出类型参数。例如:

    • initializer_list ls; // initializer_list的元素类型是string
    • initializer_list li; // initializer_list的元素类型是int
  • initializer_list比较特殊的一点是,其对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

  • 含有initializer_list形参的函数也可以同时拥有其他形参

#include <iostream>
#include <initializer_list>using namespace std;void printf_msg(string name ,initializer_list<string> il)
{cout << "[" << name << "] ";for (auto temp : il)cout << temp << " ";cout << endl;cout << "initializer_list size: " << il.size() <<endl;
}
int main()
{printf_msg("Tom", {"hello","world","!"});  // initializer_list 传入3个实参printf_msg("Peter", {"very","good"});   // initializer_list 传入2个实参
}

输出结果为:

[Tom] hello world !
initializer_list size: 3
[Peter] very good
initializer_list size: 2

内联函数

声明时使用关键字 inline。inline关键字只是表示一个要求,编译器并不承诺将inline修饰的函数作为内联。

编译时在调用处用函数体进行替换,节省了参数传递、控制转移等开销

注意:

  • 内联函数体内不能有循环语句和switch语句;
  • 内联函数的定义必须出现在内联函数第一次被调用之前;
  • 对内联函数不能进行异常接口声明。

constexpr函数

constexpr函数语法规定

  • constexpr修饰的函数在其所有参数都是constexpr时,一定返回constexpr;
  • 函数体中必须有且仅有一条return语句。

constexpr函数举例

  • constexpr int get_size() { return 20; }
  • constexpr int foo = get_size(); //正确:foo是一个常量表达式

有如下代码:

constexpr int myFunc()
{return 1;
}
constexpr int i = myFunc() * 4;

此时,编译器会将myFunc()函数用其返回值1来代替,在编译时就可知i的值是4。

注意事项

  • constexpr函数的返回值类型必须是字面值类型
int g_i = 1;
constexpr int myFunc()
{return g_i;
}

此时的返回值g_i不是字面值类型,因此报错信息为“error C3256: “g_i”: 变量使用不生成一个常量表达式”。

  • constexpr函数的形参可以是非常量,但是实参必须是常量
constexpr int myFunc(int i)
{return i;
}
constexpr int i = myFunc(1) * 4;

此时,myFunc()函数的实参是常量,在编译时可以直接被替换为1,程序不报错。

int j = 2;
constexpr int i = myFunc(j) * 4;

此时,myFunc()函数的实参是非常量,程序报错信息为“error C2131: 表达式的计算结果不是常数”,将j定义为const int则不报错。

  • 函数体中必须有且只有一条return语句
    如下代码
constexpr int myFunc(int i)
{int j;return i;
}

程序的报错信息为“error C3250: “j”: 不允许在“constexpr”函数体中进行声明”。

带默认参数值的函数

  • 默认参数值

可以预先设置默认的参数值,调用时如给出实参,则采用实参值,否则采用预先设置的默认参数值。

例:

int add(int x = 5,int y = 6) {return x + y;}int main() {add(10,20); //10+20add(10);   //10+6add();    //5+6}
  • 默认参数值的说明次序

默认参数的形参必须列在形参列表的最右,即默认参数值的右面不能有无默认值的参数;

调用时实参与形参的结合次序是从左向右

例:

int add(int x, int y = 5, int z = 6);//正确int add(int x = 1, int y = 5, int z);//错误int add(int x = 1, int y, int z = 6);//错误
  • 默认参数值与函数的调用位置

如果一个函数有原型声明,且原型声明在定义之前,则默认参数值应在函数原型声明中给出;如果只有函数的定义,或函数定义在前,则默认参数值可以函数定义中给出。

习惯像这样在函数的定义处,在形参表中以注释来说明参数的默认值,是一种好习惯。

int add(int x=5,int y=6);//默认形参值在函数原型中给出
int main() {add();return 0;
}
int add(int x/*=5*/,int y/*=6*/){//这里不能再出现默认形参,但为了清晰,可以通过注释说明默认形参return x+y;
}

函数重载

C++允许功能相近的函数在相同的作用域内以相同函数名声明,从而形成重载。方便使用,便于记忆。这是一种静态多态性,是在编译阶段实现的多态性。

  • 重载函数的形参必须不同: 个数不同类型不同。不以函数值返回类型来区分函数

  • 编译程序将根据实参和形参的类型及个数的最佳匹配来选择调用哪一个函数。注意带有默认参数的同名函数容易引起混淆

  • 不要将不同功能的函数声明为重载函数,以免出现调用结果的误解、混淆。

系统函数

标准c++函数

指与操作系统环境无关的系统函数,具有较强的可移植性。可以通过cppreference访问查看

非标准c++函数

Linux 下的系统相关函数的头文件,一般是操作系统附带的,查看这些函数的用法,可以使用 man 命令。例如,欲查询 fork 函数的用法,在命令行输入 man fork 即可


第4章 类与对象

面向对象程序设计的基本特点

抽象

对同一类对象的共同属性和行为进行概括,形成类。

  • 先注意问题的本质及描述,其次是实现过程或细节。
  • 数据抽象:描述某类对象的属性或状态(对象相互区别的物理量)。
  • 代码抽象:描述某类对象的共有的行为特征或具有的功能。
  • 抽象的实现:类。

抽象实例——钟表

数据抽象:

int hour, int minute, int second

代码抽象:

setTime(), showTime()

class Clock {public:void setTime(int newH, int newM, int newS);void showTime();private:int hour, minute, second;};

封装

将抽象出的数据、代码封装在一起,形成类。

目的:增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限,来使用类的成员。

实现封装:类声明中的{}

例:

class Clock {public: void setTime(int newH, int newM, int newS);void showTime();private: int hour, minute, second;};

继承

在已有类的基础上,进行扩展形成新的类。

详见第7章

多态

多态:同一名称,不同的功能实现方式。

目的:达到行为标识统一,减少程序中标识符的个数

实现:重载函数和虚函数——见第8章

从广义上说,多态性是指一段程序能够处理多种类型对象的能力。在 C++语言中,这种多态性可以通过强制多态、重载多态、类型参数化多态、包含多态 4 种形式来实现。
强制多态是通过将一种类型的数据转换成另一种类型的数据来实现的,也就是前面介绍过的数据类型转换(隐式或显式)。重载是指给同一个名字赋予不同的含义,在第 3 章介绍过函数重载,第 8 章还将介绍运算符重载。这两种多态属于特殊多态性,只是表面的多态性。
包含多态和类形参数化多态属于一般多态性,是真正的多态性。C++中采用虚函数实现包含多态。虚函数是多态性的精华,将在第 8 章介绍。模板是 C++实现参数化多态性的工具,分为函数模板类模板两种,将在第 9 章介绍。

类和对象的定义

  • 对象是现实中的对象在程序中的模拟。
  • 类是同一类对象的抽象,对象是类的某一特定实体。
  • 定义类的对象,才可以通过对象使用类中定义的功能。

对象所占据的内存空间只是用于存放数据成员,函数成员不在每一个对象中存储副本每个函数的代码在内存中只占据一份空间

设计类就是设计类型

  • 此类型的“合法值”是什么?
  • 此类型应该有什么样的函数和操作符?
  • 新类型的对象该如何被创建和销毁?
  • 如何进行对象的初始化和赋值?
  • 对象作为函数的参数如何以值传递?
  • 谁将使用此类型的对象成员?

类定义的语法形式

class 类名称 {public://公有成员(外部接口)private://私有成员protected://保护型成员
};

类内初始值

  • 可以为数据成员提供一个类内初始值
  • 在创建对象时,类内初始值用于初始化数据成员(在构造函数没有初始化数据成员的情况下
  • 没有初始值的成员将被默认初始化。
  • 类内初始值举例
class Clock {public:void setTime(int newH, int newM, int newS);void showTime();private:int hour = 0, minute = 0, second = 0;}; //注意大括号末尾的分号

类成员的访问控制

  • 公有类型成员

在关键字public后面声明,它们是类与外部的接口,任何外部函数都可以访问公有类型数据和函数。

  • 私有类型成员

在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。(友元是例外)

如果紧跟在类名称的后面声明私有成员,则关键字private可以省略。

  • 保护类型成员

与private类似,其差别表现在继承派生时对派生类的影响不同,详见第七章。

对象定义的语法

类名 对象名;

例:Clock myClock;

类成员的访问权限

  • 类中成员互相访问

直接使用成员名访问

在成员函数中引用其他对象的属性和调用其他对象的方法时,都需要使用“.”操作符(可以使用“.”操作符直接访问该类其他对象的私有成员)。注意,在类的成员函数中,既可以访问目的对象的私有成员,又可以访问当前类的其他对象的私有成员。

  • 类外访问

使用“对象名.成员名”方式访问 public 属性的成员

类的成员函数

  • 在类中说明函数原型;

  • 可以在类外给出函数体实现,并在函数名前使用类名加以限定;

  • 也可以直接在类中给出函数体,形成内联成员函数;

  • 允许声明重载函数和带默认参数值的函数。

内联成员函数

  1. 为了提高运行时的效率,对于较简单的函数可以声明为内联形式。

  2. 内联函数体中不要有复杂结构(如循环语句和switch语句)。

  3. 在类中声明内联成员函数的方式:

  • 将函数体放在类的声明中。

  • 使用inline关键字。

构造函数

  • 类中的特殊函数
  • 用于描述初始化算法(对类的对象初始化)

构造函数的作用

在对象被创建时使用特定的值构造对象,将对象初始化为一个特定的初始状态。

构造函数的形式

  • 函数名与类名相同

  • 不能定义返回值类型,也不能在函数体中有return语句

  • 可以有形式参数,也可以没有形式参数

  • 可以是内联函数

  • 可以重载

  • 可以带默认参数值

构造函数的调用时机

  • 在对象创建时被自动调用

  • 例如: Clock myClock(0, 0, 0)

默认构造函数

调用时可以不要实参的构造函数,称为default constructor

  • 参数表为空的构造函数
  • 全部参数都有默认值的构造函数
Clock();
Clock(int newH = 0, int newM = 0, int newS = 0);
// 两者都是默认构造函数,但如果都在类中同时出现,将产生编译错误

隐含生成的构造函数

  • 如果程序中未定义构造函数,编译器将自动生成一个默认构造函数

  • 参数列表为空,不为数据成员设置初始值

  • 如果类内定义了成员的初始值,则使用类内定义的初始值

    例如 int hour = 0, int minute = 0, int second = 0;

  • 如果没有定义类内的初始值,则以默认方式初始化

    • 基本类型的数据默认初始化的值时不确定的

    • 如果数据成员为对象,则由它所属的类来决定

“=default”

如果程序中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数。如果此时依然希望编译器隐含生成默认构造函数,可以使用 “=default”

例:

class Clock {public:Clock() =default; //指示编译器提供默认构造Clock(int newH, int newM, int newS); //构造函数
private:int hour, minute, second;
}

构造函数初始化列表

Clock::Clock(int newH, int newM, int newS) : hour(newH), minute(newM), second(newS) {std::cout << "Call constructor function...\n";
}
//注意":"后的写法,此为初始化列表,效率更高

委托构造函数

委托构造函数(Delegating Constructor)由C++11引入,是对C++构造函数的改进,允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。

通俗来讲,一个委托构造函数使用它所属的类的其他构造函数执行自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。和其他构造函数一样,一个委托构造函数也有一个成员初始化列表和一个函数体,成员初始化列表只能包含一个其它构造函数不能再包含其它成员变量的初始化,且参数列表必须与构造函数匹配

首先看一下一个不使用委托构造函数造成代码冗余的例子。

class Foo
{public:Foo() : type(4), name('x') {initRest();}Foo(int i) : type(i), name('x') {initRest();}Foo(char c) : type(4), name(c) {initRest();}private:void initRest() {/* init othre members */}int type;char name;//...
};

从上面的代码片段可以看出,类Foo的三个构造函数除了参数不同,初始化列表、函数体基本相同,其代码存在着很多重复。在C++11中,我们可以使用委托构造函数来减少代码重复,精简构造函数。

class Foo
{public:Foo() {initRest(); }Foo(int i) : Foo() {type = i;}Foo(char e) : Foo() {name = e;}
private:void initRest() { /* init othre members */}int type{1};char name{'a'};
};

一个委托构造函数想要委托另一个构造函数,那么被委托的构造函数应该包含较大数量的参数,初始化较多的成员变量。而且在委托其他构造函数后,不能再进行成员列表初始化,而只能在函数体内对其他成员变量进行赋值

复制构造函数

复制构造函数定义

复制构造函数是一种特殊的构造函数,其形参为本类的对象引用(注意为const常引用,写法为const 类名 &对象名)。作用是用一个已存在的对象去初始化同类型的新对象。

class 类名 {public :类名(形参);//构造函数类名(const 类名 &对象名);//复制构造函数//   ...};类名::类(const 类名 &对象名){ //复制构造函数的实现函数体
}

隐含的复制构造函数

如果程序员没有为类声明拷贝初始化构造函数,则编译器自己生成一个隐含的复制构造函数。

这个构造函数执行的功能是:用作为初始值的对象的每个数据成员的值,初始化将要建立的对象的对应数据成员。一对一对应复制。

“=delete”

如果不希望对象被复制构造

C++98做法:将复制构造函数声明为private,并且不提供函数的实现。

C++11做法:用“=delete”指示编译器不生成默认复制构造函数。

例:

class Point {  //Point 类的定义public:Point(int xx=0, int yy=0) { x = xx; y = yy; }  //构造函数,内联Point(const Point& p) =delete; //指示编译器不生成默认复制构造函数private:int x, y; //私有数据};

复制构造函数被调用的三种情况

  1. 定义一个对象时,以本类另一个对象作为初始值,发生复制构造(即:当用类的一个对象去初始化该类的另一个对象时);
  2. 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造(只有把对象用值传递时才会调用复制构造函数,如果传递引用,则不会调用复制构造函数。由于这一原因,传递比较大的对象时,传递引用会比传值的效率高很多。);
  3. 如果函数的返回值是类的对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。
    • 这种情况也可以通过移动构造避免不必要的复制(第6章介绍)
Point g(){Point a(1, 2);return a;//函数的返回值是类对象,返回函数值时,调用复制构造函数
}
int main() {Point b;b = g();return 0;
}

为什么在这种情况下,返回函数值时,会调用复制构造函数呢?表面上函数 g 将 a返回给了主函数,但是 a是 g()的局部对象,离开建立它的函数 g 以后就消亡了,不可能在返回主函数后继续生存(这一点在第 5 章中将详细讲解)。所以在处理这种情况时编译系统会在主函数中创建一个无名临时对象,该临时对象的生存期只在函数调用所处的表达式中,也就是表达式“b= g()”中。执行语句“return a;”时,实际上是调用复制构造函数将a的值复制到临时对象中。函数 g 运行结束时对象 a消失,但临时对象会存在于表达式“b= g()”中。计算完这个表达式后,临时对象的使命也就完成了,该临时对象便自动消失。

析构函数

  • 完成对象被删除前的一些清理工作。
  • 在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间。
  • 如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数,其函数体为空且不能有return语句,参数表也必须为空。

类的组合

组合的概念

  • 类中的成员是另一个类的对象。
  • 可以在已有抽象的基础上实现更复杂的抽象。

类组合的构造函数设计

  • 原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化
  • 声明形式:
类名::类名(对象成员所需的形参,本类成员形参): 对象1(参数),对象2(参数),...... {//函数体其他语句
}

构造组合类对象时的初始化次序

  • 首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序,与初始化列表中的次序无关。

    • 成员对象构造函数调用顺序:按对象成员的声明顺序,先声明者先构造。

    • 初始化列表中未出现的成员对象,调用用默认构造函数(即无形参的)初始化

  • 处理完初始化列表之后,再执行构造函数的函数体。

前向引用声明

用于解决两个类相互引用的问题

  • 类应该先声明,后使用
  • 如果需要在某个类的声明之前,引用该类,则应进行前向引用声明。
  • 前向引用声明只为程序引入一个标识符,但具体声明在其他地方。
  • 例:
class B;  //前向引用声明
class A {
public:void f(B b);
};
class B {
public:void g(A a);
};

前向引用声明注意事项

  • 使用前向引用声明虽然可以解决一些问题,但它并不是万能的
  • 在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。
  • 当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。

class Fred; //前向引用声明
class Barney {Fred x; //错误:类Fred的声明尚不完善
};
class Fred {Barney y;
};

编译的时候编译器报错 为什么呢?

因为还没有定义Fred类,即使说明了这是一个类名也没有用,在编译Barney类的时候要把x作为Fred类的对象,这时候最起码得知道Fred类它占多少字节,像这样的细节信息不知道就没有在这里面定义Fred类对象,所以在完整的定义一个类之前用一个前向引用声明也只声明了这个类名而已,只能用这个名字不能涉及到它的任何细节。

结构体

  • 结构体是一种特殊形态的类

    • 与类的唯一区别:类的缺省访问权限是private,结构体的缺省访问权限是public

    • 结构体存在的主要原因:与C语言保持兼容

  • 什么时候用结构体而不用类

    • 定义主要用来保存数据、而没有什么操作的类型
    • 人们习惯将结构体的数据成员设为公有,因此这时用结构体更方便

结构体的定义

struct 结构体名称 {公有成员
protected:保护型成员
private:私有成员
};

结构体的初始化

  • 如果一个结构体的全部数据成员都是公共成员,并且没有用户定义的构造函数,没有基类和虚函数(基类和虚函数将在后面的章节中介绍),这个结构体的变量可以用下面的语法形式赋初值
类型名 变量名 = { 成员数据1初值, 成员数据2初值, …… };

例4-7用结构体表示学生的基本信息

#include <iostream>
#include <iomanip>
#include <string>
using namespace std;struct Student {    //学生信息结构体int num;       //学号string name;    //姓名,字符串对象,将在第6章详细介绍char sex;     //性别int age;        //年龄
};int main() {Student stu = { 97001, "Lin Lin", 'F', 19 };cout << "Num:  " << stu.num << endl;cout << "Name: " << stu.name << endl;cout << "Sex:  " << stu.sex << endl;cout << "Age:  " << stu.age << endl;return 0;
}运行结果:
Num:  97001
Name: Lin Lin
Sex:  F
Age:  19

联合体

声明形式

union 联合体名称 {公有成员
protected:保护型成员
private:私有成员
};

特点

  • 成员共用同一组内存单元
  • 任何两个成员不会同时有效

联合体的内存分配

  • 举例说明:
union Mark { //表示成绩的联合体char grade;   //等级制的成绩bool pass;  //只记是否通过课程的成绩int percent;   //百分制的成绩
};

无名联合

  • 例:
union {int i;float f;
}
在程序中可以这样使用:
i = 10;
f = 2.2; //i和f共用一块存储空间,即修改f值也会修改i的值

例:使用联合体保存成绩信息,并且输出。

#include <iostream>
using namespace std;
class ExamInfo {
private:string name;    //课程名称enum { GRADE, PASS, PERCENTAGE } mode;//计分方式union {char grade;    //等级制的成绩bool pass;  //只记是否通过课程的成绩int percent;   //百分制的成绩};
public://三种构造函数,分别用等级、是否通过和百分初始化ExamInfo(string name, char grade): name(name), mode(GRADE), grade(grade) { }ExamInfo(string name, bool pass): name(name), mode(PASS), pass(pass) { }ExamInfo(string name, int percent): name(name), mode(PERCENTAGE), percent(percent) { }void show();
}void ExamInfo::show() {cout << name << ": ";switch (mode) {case GRADE: cout << grade;  break;case PASS: cout << (pass ? "PASS" : "FAIL"); break;case PERCENTAGE: cout << percent; break;}cout << endl;
}int main() {ExamInfo course1("English", 'B');ExamInfo course2("Calculus", true);ExamInfo course3("C++ Programming", 85);course1.show();course2.show();course3.show();return 0;
}
运行结果:
English: B
Calculus: PASS
C++ Programming: 85

枚举类

枚举类定义

  • 语法形式

enum class 枚举类型名: 底层类型 {枚举值列表};

例:

enum class Type { General, Light, Medium, Heavy};enum class Type: char { General, Light, Medium, Heavy};enum class Category { General=1, Pistol, MachineGun, Cannon};

枚举类的优势

  • 强作用域,其作用域限制在枚举类中。

    • 例:使用Type的枚举值General: Type::General
  • 转换限制,枚举类对象不可以与整型隐式地互相转换。

  • 可以指定底层类型

    • 例:enum class Type: char { General, Light, Medium, Heavy};

例:枚举类举例

#include<iostream>using namespace std;enum class Side{ Right, Left };enum class Thing{ Wrong, Right }; //不冲突int main(){Side s = Side::Right;Thing w = Thing::Wrong;cout << (s == w) << endl; //编译错误,无法直接比较不同枚举类return 0;}

第5章 数据的共享与保护

标识符的作用域与可见性

作用域讨论的是标识符的有效范围,可见性讨论的是标识符是否可以被引用。

作用域

作用域是一个标识符在程序正文中有效的区域。C++中标识符的作用域有函数原型作用域局部作用域(块作用域)类作用域命名空间作用域

1.函数原型作用域

C++程序中最小的作用域。在函数原型声明时形式参数的作用范围就是函数原型作用域。

例:

double area(double radius);//标识符radius

注:标识符radius的作用域范围就是在函数area形参列表的左右括号之间,在程序的其他地方不能引用这个标识符。

​ 在函数原型的形参列表中起作用的只是形参类型,标识符并不起作用,因此可以省略标识符,但为了程序的可读性,通常在函数原型声明时给出标识符。

2.局部作用域

函数形参列表中的形参的作用域,从形参列表中的声明处开始,到整个函数体结束之处为止。

函数体内声明的变量,其作用域从声明处开始,一直到声明所在的块结束的大括号为止。

具有局部作用域的变量也称局部变量

void fun(int a){//a的作用域整个函数体int b=a;//b的作用域大括号里cin>>b;if (b<0){int c;//c的作用域 if的大括号里...}
}

再例:

#include <iostream>
using namespace std;void anotherFunction() ; //函数原型
int main()
{int num = 1; //主函数main中的numcout << "In main, num is " << num << endl;anotherFunction();cout << "Back in main, num is still " << num << endl;return 0;
}
void anotherFunction()
{int num = 20; //函数anotherFunction中的numcout << "In anotherFunction, num is " << num << endl;
}

注:虽然有两个名为 num 的变量,但是程序在同一时间只能“看到”其中一个,因为它们在不同的函数中。

两个函数的封闭性质,“{}”分隔变量的作用域。第一个main变量仅在main函数中可见;第二个num仅在anotherFunction函数中可见。

3.类的作用域

类可以被看成一组有名成员的集合,类X的成员m具有类的作用域,对m的访问有如下三种方式:、

①如果X的成员函数中没有声明同名的局部作用域标识符,那么可以直接访问m。也就是说m在这样的函数中都起作用。

Clock globClock;
globClock.showtime();//对象的成员函数具有类的作用域

②通过表达式x.mX::m。这是访问对象成员的最基本方法。X::m的方式用于访问类的静态成员

void Clock::showtime()

③使用ptr->m,其中ptr为指向X类的一个对象的指针。

Student *student = new Student();
student->show();

4.命名空间作用域

命名空间的语法形式:

namespace 命名空间名{命名空间内的各种声明(函数声明、类声明......)
}

注:

一个命名空间确定了一个命名空间的作用域,凡是在该命名空间之内声明的、不属于前面所述各个作用域的标识符,都属于该命名空间作用域。

①如需在该命名空间内需要引用其他命名空间的标识符,语法形式如下:

命名空间名::标识符

namespace SomeNs{class SomeClass{...};
}
//如需引用类名SomeClass或函数名someFunc
SomeNs::SomeClass obj1;//声明一个SomeNS型的对象obj1

②为了避免标识符前总使用上面的命名空间限定显得冗长,C++提供了using语句

两种形式

using 命名空间名::标识符名;//将指定的标识符暴露在当前作用域内,使得在当前作用域内可以直接使用该标识符
using namespace 命名空间名;//将指定命名空间内的所有标识符暴露在当前作用域内,使得在该命名空间内可以直接使用任何标识符

③命名空间允许嵌套

namespace OuterNs{namespace InnerNs{class SomeClass(...);}
}

④特殊的命名空间:全局命名空间和匿名命名空间

全局命名空间:是默认的的命名空间,在显式声明的命名空间之外生命的标识符都在一个全局命名空间中

匿名命名空间:是一个需要显式声明的没有名字的命名空间,例如:

namespace{匿名命名空间内的各种声明(函数声明、类声明、......)
}

可见性

定义:程序运行到某一点,能够引用到的标识符,就是该处可见的标识符。

作用域可见性的一般规则如下:

①标识符要声明在前,引用在后

②在同一作用域中不能声明同名的标识符

在没有互相包含关系的不同的作用域中声明的同名标识符,互不影响

如果在两个或者多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见

三、程序实例

#include <iostream>
using namespace std;
int i;                           //在全局命名空间中的全局变量
namespace Ns{int j;                       //在Ns命名空间中的全局变量
}
int main(){i=5;                         //为全局变量i赋值Ns::j=6;                     //为全局变量j赋值{using namespace Ns;      //使当前块中可以直接引用Ns命名空间的标识符int i;                   //局部变量,局部作用域i=7;cout<<"i="<<i<<endl;    cout<<"j="<<j<<endl;     }cout<<"i="<<i<<endl;        return 0;
}

注意:5-1中声明的全局变量就是具有命名空间的作用域,他们在整个文件中都有效。(具有命名空间作用域的变量也成为全局变量

①变量 i 是全局命名空间,有效范围直到文件尾。

②在主函数开始处给 i 赋初值5,接下来又声明了同名变量并赋初值7。第一次输出的结果是7,因为具有局部作用域的变量 i 把具有命名空间作用域的 i 隐藏了,于是具有命名空间作用域的 i 变得不可见。

③第一个块运行结束,输出的 i 的值为7,因为具有局部作用域的 i 不在有效范围之内了,现在处在有效范围内的变量只有具有命名空间作用域的那个变量。

④j被声明在命名空间 Ns 中,在主函数中通过 Ns::j 的方式引用,为其赋值。

对象的可见性

静态生存期

  • 这种生存期与程序的运行期相同。(离开其作用域时并不消亡)
  • 在文件作用域中声明的对象具有这种生存期。
  • 在函数内部声明静态生存期对象,要冠以关键字static 。

动态生存期

  • 块作用域中声明的,没有用static修饰的对象是动态生存期的对象(习惯称局部生存期对象)。
  • 开始于程序执行到声明点时,结束于命名该标识符的作用域结束处。

例:注意注释的内容

#include<iostream>using namespace std;int i = 1; // i 为全局变量,具有静态生存期。void other() {static int a = 2;static int b;// a,b为静态局部变量,具有全局寿命,局部可见。//只第一次进入函数时被初始化。不初始化int变量默认是0int c = 10; // C为局部变量,具有动态生存期,//每次进入函数时都初始化。a += 2; i += 32; c += 5;cout<<"---OTHER---\n";cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;b = a;}int main() {static int a;//静态局部变量,有全局寿命,局部可见。int b = -10; // b, c为局部变量,具有动态生存期。int c = 0;cout << "---MAIN---\n";cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;c += 8; other();cout<<"---MAIN---\n";cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;i += 10; other();  return 0;}

类的静态成员

静态数据成员

  • 用关键字static声明

  • 为该类的所有对象共享,静态数据成员具有静态生存期。

  • 必须在类外定义和初始化,用(::)来指明所属的类。

#include <iostream>using namespace std;class Point {   //Point类定义public:    //外部接口Point(int x = 0, int y = 0) : x(x), y(y) { //构造函数//在构造函数中对count累加,所有对象共同维护同一个countcount++;}Point(Point &p) {  //复制构造函数x = p.x;y = p.y;count++;}~Point() { count--; }int getX() { return x; }int getY() { return y; }void showCount() {      //输出静态数据成员,不能加const关键字修饰cout << " Object count = " << count << endl;}private:   //私有数据成员int x, y;static int count;    //静态数据成员声明,用于记录点的个数//别忘了在类外进行初始化,初始化时并不需要加static};int Point::count = 0;//静态数据成员定义和初始化,使用类名限定int main() {    //主函数Point a(4, 5);   //定义对象a,其构造函数回使count增1cout << "Point A: " << a.getX() << ", " << a.getY();a.showCount(); //输出对象个数Point b(a); //定义对象b,其构造函数回使count增1cout << "Point B: " << b.getX() << ", " << b.getY();b.showCount();    //输出对象个数return 0;}

运行结果:

 Point A: 4, 5 Object count=1Point B: 4, 5 Object count=2

存在问题:如果一个点都没有,怎么得知点的个数(这时无法调用showCount函数)

静态函数成员

  • 类外代码可以使用类名和作用域操作符来调用静态成员函数。例:Point::showCount()

  • 静态成员函数主要用于处理该类的静态数据成员,可以直接调用静态成员函数。

  • 如果访问非静态成员,要通过对象来访问。

  • 静态函数成员不能声明const

例5-5具有静态数据、函数成员的 Point类

#include <iostream>using namespace std;class Point {  public:   Point(int x = 0, int y = 0) : x(x), y(y) { count++; }//构造函数Point(Point &p) {  //复制构造函数x = p.x;y = p.y;count++;}~Point() { count--; }int getX() { return x; }int getY() { return y; }static void showCount() {  cout << " Object count = " << count << endl;}private:   int x, y;static int count;    //静态数据成员声明,用于记录点的个数};int Point::count = 0;//静态数据成员定义和初始化,使用类名限定int main() {   Point a(4, 5);   //定义对象a,其构造函数回使count增1cout << "Point A: " << a.getX() << ", " << a.getY();Point::showCount();    //输出对象个数//也可以用a.showCount()访问Point b(a); //定义对象b,其构造函数回使count增1cout << "Point B: " << b.getX() << ", " << b.getY();Point::showCount();    //输出对象个数return 0;}

类的友元

友元是C++提供的一种破坏数据封装和数据隐藏的机制。

通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息。

可以使用友元函数和友元类。

为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。

友元函数

  • 友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员

  • 作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择。

  • 访问对象中的成员必须通过对象名。

5-6 使用友元函数计算两点间的距离

#include <iostream>#include <cmath>using namespace std;class Point { //Point类声明public: //外部接口Point(int x=0, int y=0) : x(x), y(y) { }int getX() { return x; }int getY() { return y; }friend float dist(Point &a, Point &b); //友元函数,注意需要传递对象作为参数,并且为引用传递(这里应该传常引用)private: //私有数据成员int x, y;};float dist( Point& a, Point& b) {double x = a.x - b.x;double y = a.y - b.y;return static_cast<float>(sqrt(x * x + y * y));}int main() {Point p1(1, 1), p2(4, 5);cout <<"The distance is: ";cout << dist(p1, p2) << endl;return 0;}

友元类

若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。

声明语法:将友元类名在另一个类中使用friend修饰说明。

class A {friend class B;public:void display() {cout << x << endl;}private:int x;};class B {public:void set(int i);void display();private:A a;};void B::set(int i) {a.x=i; //直接访问A的私有成员}void B::display() {a.display();};

类的友元关系注意事项

第一,友元关系是不能传递的。如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。第二,友元关系是单向的,如果声明 B 类是 A 类的友元,B 类的成员函数就可以访问 A 类的私有和保护数据,但 A 类的成员函数却不能访问 B 类的私有、保护数据。第三,友元关系是不被继承的,如果类B 是类 A 的友元,类 B 的派生类并不会自动成为类 A的友元。打个比方说,就好像别人信任你,但是不见得信任你的孩子

共享数据的保护

  • 对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)。

  • 对于不改变对象状态的成员函数应该声明为常函数

常类型分为:

  • 常对象:必须进行初始化,不能被更新。它的数据成员值在对象的整个生存期间内不能被改变。

    const 类名 对象名

    在定义一个变量或常量时为它指定初值叫做初始化,而在定义一个变量或常量以后使用赋值运算符修改它的值叫做赋值,请勿将初始化与赋值混淆。

    语法如何保障类类型的常对象的值不被改变呢?改变对象的数据成员值有两个途径:一是通过对象名访问其成员对象,由于常对象的数据成员都被视同为常量,这时语法会限制不能赋值。二是在类的成员函数中改变数据成员的值,然而几乎无法预料和统计哪些成员函数会改变数据成员的值,对此语法只好规定不能通过常对象调用普通的成员函数。可是这样一来,常对象还有什么用呢?它没有任何可用的对外接口。别担心,办法还是有的,在 5.5.2 节中将介绍专门为常对象定义的常成员函数

  • 常成员

    • 用const进行修饰的类成员:常数据成员常函数成员
  • 常引用:被引用的对象不能被更新。

    const 类型说明符 &引用名

  • 常数组:数组元素不能被更新(详见第6章)。

    类型说明符 const 数组名[大小]...

  • 常指针:指向常量的指针(详见第6章)。

常对象

用const修饰的对象

例:

class A{public:A(int i,int j) {x=i; y=j;}...private:int x,y;};A const a(3,4); //a是常对象,不能被更新

思考:哪些操作有试图改变常对象状态的危险?

常成员

用const修饰的对象成员

常成员函数

  • 使用const关键字说明的函数。

  • 常成员函数不更新对象的数据成员。

  • 常成员函数说明格式:

    类型说明符 函数名(参数表)const;

这里,const是函数类型的一个组成部分,因此在实现部分也要带const关键字。

  • const关键字可以被用于参与对重载函数的区分

    • 通过常对象只能调用它的常成员函数。

    • 常成员函数可以被非常对象调用,但常对象不可调用非常成员函数

如果将一个对象说明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数(这就是 C++从语法机制上对常对象的保护,也是常对象唯一的对外接口方式)即没有const修饰的函数常对象(包括常对象的引用)不能调用的。

无论是否通过常对象调用常成员函数,在常成员函数调用期间,目的对象都被视同为常对象,因此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有用 co n st修饰的成员函数(这就保证了在常成员函数中不会更改目的对象的数据成员的值)

例5-7 常成员函数举例

#include<iostream>
using namespace std;class R {public:R(int r1, int r2) : r1(r1), r2(r2) { }void print();void print() const;
private:int r1, r2;
};void R::print() {cout << r1 << ":" << r2 << endl;
}void R::print() const {cout << r1 << ";" << r2 << endl;
}int main() {R a(5,4);a.print(); //调用void print()const R b(20,52); b.print(); //调用void print() constreturn 0;
}

常数据成员

使用const说明的数据成员。

例5-8 常数据成员举例

#include <iostream>
using namespace std;class A {public:A(int i);void print();
private:const int a;static const int b; //静态常数据成员
};const int A::b=10; //注意这里对静态常数据成员的类外说明及初始化A::A(int i) : a(i) { }  //这里只能用初始化列表初始化avoid A::print() {cout << a << ":" << b <<endl;
}int main() {//建立对象a和b,并以100和0作为初值,分别调用构造函数,
//通过构造函数的初始化列表给对象的常数据成员赋初值A a1(100), a2(0);a1.print();a2.print();return 0;
}

常引用

如果在声明引用时用const修饰,被声明的引用就是常引用。

常引用所引用的对象不能被更新。

如果用常引用做形参,便不会意外地发生对实参的更改。常引用的声明形式如下:

const 类型说明符 &引用名;

例5-9 常引用作形参

#include <iostream>
#include <cmath>
using namespace std;class Point { //Point类定义
public:     //外部接口Point(int x = 0, int y = 0): x(x), y(y) { }int getX() { return x; }int getY() { return y; }friend float dist(const Point &p1,const Point &p2);private:     //私有数据成员int x, y;
};float dist(const Point &p1, const Point &p2) {double x = p1.x - p2.x; double y = p1.y - p2.y;return static_cast<float>(sqrt(x*x+y*y));
}int main() { //主函数const Point myp1(1, 1), myp2(4, 5);  cout << "The distance is: ";cout << dist(myp1, myp2) << endl;return 0;
}

多文件结构和编译预处理命令

C++程序的一般组织结构

一个工程可以划分为多个源文件:

  • 类声明文件(.h文件)

  • 类实现文件(.cpp文件)

  • 类的使用文件(main()所在的.cpp文件)

    利用工程来组合各个文件。

例 5-10 多文件的工程

//文件1,类的定义,Point.h
class Point { //类的定义
public:     //外部接口Point(int x = 0, int y = 0) : x(x), y(y) { }Point(const Point &p);~Point() { count--; }int getX() const { return x; }int getY() const { return y; }static void showCount();     //静态函数成员private:     //私有数据成员int x, y;static int count; //静态数据成员
};//文件2,类的实现,Point.cpp
#include "Point.h"
#include <iostream>
using namespace std;int Point::count = 0;      //使用类名初始化静态数据成员Point::Point(const Point &p) : x(p.x), y(p.y) {count++;
}void Point::showCount() {cout << " Object count = " << count << endl;
}//文件3,主函数,5_10.cpp
#include "Point.h"
#include <iostream>
using namespace std;int main() {Point a(4, 5);   //定义对象a,其构造函数使count增1cout <<"Point A: "<<a.getX()<<", "<<a.getY();Point::showCount();   //输出对象个数Point b(a);     //定义对象b,其构造函数回使count增1cout <<"Point B: "<<b.getX()<<", "<<b.getY();Point::showCount();   //输出对象个数return 0;
}


注意:内联函数比较特殊,由于它的内容需要嵌入到每个调用它的函数之中,所以对于那些需要被多个编译单元调用的内联函数,它们的代码应该被各个编译单元可见,这些内联函数的定义应当出现在头文件中
决定一个声明放在源文件中还是头文件中的一般原则是,将需要分配空间的定义放在源文件中,例如函数的定义(需要为函数代码分配空间)、命名空间作用域中变量的定义(需要为变量分配空间)等;而将不需要分配空间的声明放在头文件中,例如类声明、外部函数的原型声明、外部变量的声明(外部函数和外部变量将在 5.6.2 节中详细讨论)、基本数据类型常量的声明等

外部变量

  • 如果一个变量除了在定义它的源文件中可以使用外,还能被其它文件使用,那么就称这个变量是外部变量。

  • 文件作用域中定义的变量,默认情况下都是外部变量,但在其它文件中如果需要使用这一变量,需要用extern关键字加以声明。

外部函数

  • 在所有类之外声明的函数(也就是非成员函数),都是具有文件作用域的。

  • 这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明(即声明函数原型)即可。也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的。

将变量和函数限制在编译单元内

命名空间作用域中声明的变量和函数,在默认情况下都可以被其他编译单元访问,但有时并不希望一个源文件中定义的命名空间作用域的变量和函数被其他源文件引用。这种需求主要是出于两个原因,一是出于安全性考虑,不希望将一个只会在文件内使用的内部变量或函数暴露给其他编译单元,就像不希望暴露一个类的私有成员一样;二是,对于大工程来说,不同文件之中的、只在文件内使用的变量名很容易重名,如果将它们都暴露出来,在连接时很容易发生名字冲突。其中一种解决方案是使用static关键字

另一种是使用匿名的命名空间:在匿名命名空间中定义的变量和函数,都不会暴露给其它的编译单元。

  namespace {     //匿名的命名空间
​     int n;
​     void f() {​           n++;
​     }}

这里被“namespace { …… }”括起的区域都属于匿名的命名空间。

标准C++库

标准C++类库是一个极为灵活并可扩展的可重用软件模块的集合。标准C++类与组件在逻辑上分为6种类型:

  • 输入/输出类

  • 容器类与抽象数据类型

  • 存储管理类

  • 算法

  • 错误处理

  • 运行环境支持

编译预处理

  • #include 包含指令

    将一个源文件嵌入到当前源文件中该点处。

  • #include<文件名>

    按标准方式搜索,文件位于C++系统目录的include子目录下

  • #include"文件名"

    首先在当前目录中搜索,若没有,再按标准方式搜索。

  • #define 宏定义指令

    • 定义符号常量,很多情况下已被const定义语句取代。

    • 定义带参数宏,已被内联函数取代。

  • #undef

    删除由#define定义的宏,使之不再起作用。

以下关于外部变量和外部函数的说法,错误的是:

1、外部变量的声明可以是引用性的声明

2、静态变量和静态函数即使使用extern声明,它们的使用范围仍然被限定在定义文件中

3、外部变量可以为多个源文件所共享

4、外部函数和外部变量在声明时,都不能省略关键字extern.

正确答案:4.有的不需要extern。

第6章 数组、指针和字符串

数组的存储与初始化

一维数组的初始化

数组元素在内存中顺次存放,它们的地址是连续的。元素间物理地址上的相邻,对应着逻辑次序上的相邻。

二维数组的存储

按行存放。即行优先存储

例如对于float a[3][4];
即可理解为
a[0] ----- a00 a01 a02 a03
a[1] ----- a10 a11 a12 a13
a[2] ----- a20 a21 a22 a23
数组a的存储顺序为
a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23

数组作为函数参数

  • 数组元素作实参,与单个变量一样。

  • 数组名作参数,形、实参数都应是数组名(实质上是地址),类型要一样,传送的是数组首地址。对形参数组的改变会直接影响到实参数组。

  • 把数组作为参数时,一般不指定数组第一维的大小,即使指定,也会被忽略

对象数组

对象数组的定义与访问

定义对象数组

类名 数组名[元素个数];

访问对象数组元素

  • 通过下标访问

  • 数组名[下标].成员名

对象数组初始化

数组中每一个元素对象被创建时,系统都会调用类构造函数初始化该对象。

通过初始化列表赋值。

例:Point a[2]={Point(1,2),Point(3,4)};

如果没有为数组元素指定显式初始值,数组元素便使用默认值初始化(调用默认构造函数)。

数组元素所属类的构造函数

元素所属的类不声明构造函数,则采用默认构造函数。

各元素对象的初值要求为相同的值时,可以声明具有默认形参值的构造函数。

各元素对象的初值要求为不同的值时,需要声明带形参的构造函数。

当数组中每一个对象被删除时,系统都要调用一次析构函数。

基于范围的for循环

基于范围的 for 循环可以自动知道数组中元素的个数,所以不必使用计数器变量控制其迭代,也不必担心数组下标越界的问题。

基于范围的 for 循环使用了一个称为范围变量的内置变量。每次基于范围的 for 循环迭代时,它都会复制下一个数组元素到范围变量。例如,第一次循环迭代,范围变量将包含元素 0 的值;第二次循环迭代,范围变量将包含元素 1 的值,以此类推。

代码格式

for (dataType rangeVariable : array)statement;
  • dataType:是范围变量的数据类型。它必须与数组元素的数据类型一样,或者是数组元素可以自动转换过来的类型。
  • rangeVariable:是范围变量的名称。该变量将在循环迭代期间接收不同数组元素的值。在第一次循环迭代期间,它接收的是第一个元素的值;在第二次循环迭代期间,它接收的是第二个元素的值;以此类推。
  • array:是要让该循环进行处理的数组的名称。该循环将对数组中的每个元素迭代一次。
  • statement:是在每次循环迭代期间要执行的语句。要在循环中执行更多的语句,则可以使用一组大括号来包围多个语句。
for(int val : numbers)
{cout << "The next value is ";cout << val << endl;
}
//其中int numbers[] = {3, 6, 9};
//也可以使用 auto 关键字指定范围变量的数据类型,而不必手动指定,如for(auto val : numbers) { ... }

当基于范围的 for 循环执行时,其范围变量将仅包含一个数组元素的副本。因此,不能使用基于范围的 for 循环来修改数组的内容,除非将范围变量声明为一个引用。引用变量是其他值的一个别名,任何对于引用变量的修改都将实际作用于别名所代表的值。例如

    for (int &val : numbers) //注意&引用{cout << "Enter an integer value: ";cin >> val;}

指针初始化和赋值

指针变量的初始化

语法形式

存储类型 数据类型 *指针名=初始地址;

例:

int *pa = &a;

注意事项

  • 用变量地址作为初值时,该变量必须在指针初始化之前已声明过,且变量类型应与指针类型一致。

  • 可以用一个已有合法值的指针去初始化另一个指针变量。

  • 不要用一个内部非静态变量去初始化 static 指针。

指针变量的赋值运算

语法形式

指针名=地址

注意:“地址”中存放的数据类型与指针类型必须相符

向指针变量赋的值必须是地址常量或变量,不能是普通整数,例如:

  • 通过地址运算“&”求得已定义的变量和对象的起始地址

  • 动态内存分配成功时返回的地址

例外:整数0可以赋给指针,表示空指针。

允许定义或声明指向 void 类型的指针。该指针可以被赋予任何类型对象的地址

例: void *general;

指针空值nullptr
以往用0或者NULL去表达空指针的问题:

C/C++的NULL宏是个被有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。但是在C++的时代,这就会引发很多问题。

C++11使用nullptr关键字,是表达更准确,类型安全的空指针

指针类型注意事项(常量指针与指针常量)

  • 可以声明指向常量的指针,此时不能通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象。使用指向常量的指针,可以确保指针所指向的常量不被意外更改。如果用一般指针存放常量的地址,编译器就不能确保指针所指的对象不被更改。例如:
int a;
const int* p1 = &a;//p1是指向常量的指针
*p1=1;//编译时出错,不能通过p1改变所指的对象(注意a是变量)int b;
p1 = &b;//正确,p1本身的值可以改变
  • 声明指针类型的常量,这时指针本身的值不能被改变。例如
int* const p2 = &a;
p2 = &b;//错误,p2是指针常量,值不能改变
  • 一般情况下,指针的值只能赋给相同类型的指针。但是有一种特殊的 void 类型指针,可以存储任何类型的对象地址,就是说任何类型的指针都可以赋值给 void 类型的指针变量。经过使用类型显式转换,通过void类型的指针便可以访问任何类型的数据。void 指针一般只在指针所指向的数据类型不确定时使用。例如:
#include <iostream>
using namespace std;int main() {//void voidobject;错,不能声明void类型的变量void* pv;//对,可以声明void类型的指针int i = 5;pv = &i;//void类型指针指向整型变量int* pint = static_cast<int*>(pv);//void类型指针赋值给int类型指针cout << "*pint=" << *pint << endl;return 0; //结果是*pint=5

指针数组

指针数组:数组的元素是指针类型

例:Point *p[2]。 该数组由p[1],p[2]两个指向Point对象的指针构成。

指针作函数参数的意义

在 C 语言中,以指针作为函数的形参有三个作用:

  • 第一个作用是使实参与形参指针指向共同的内存空间,以达到参数双向传递的目的,即通过在被调函数中直接处理主调函数中的数据而将函数的处理结果返回其调用者。这个作用在 C++中已经由引用实现了,这一点在第 3 章中介绍用引用作为函数参数时已经详细介绍过。
  • 第二个作用,就是减少函数调用时数据传递的开销。这一作用在 C++中有时可以通过引用实现,有时还是需要使用指针。
  • 以指针作形参的第三个作用,是通过指向函数的指针传递函数代码的首地址

指针类型的函数

若函数的返回类型是指针,该函数就是指针类型的函数。使用指针型函数的最主要目的就是要在函数结束时把大量的数据从被调函数返回到主调函数中。

指针函数的定义形式

存储类型 数据类型 *函数名() {//函数体语句
}

注意:不要将非静态局部地址,即函数体内定义的变量或者对象用作函数的返回值。
错误的例子:在子函数中定义局部变量后将其地址返回给主函数,就是非法地址。

int main() {
int* function();
int *ptr=function();
*ptr=5; //危险的访问!
return 0;int* function() {int local=0;//非静态局部变量作用域和寿命都仅限于本函数体内return &local;
}//函数运行结束时,变量local被释放

返回的指针要确保在主调函数中是有效、合法的地址。
正确的例子:
主函数中定义的数组,在子函数中对该数组元素进行某种操作后,返回其中一个元素的地址,这就是台法有效的地址。

正确的例子:
在子函数中通过动态内存分配new操作取得的内存地址返回给主函数是合法有效的,但是内存分配和释放不在同一级别,要注意不能忘记释放,避免内存泄漏。

指向函数的指针

函数指针的定义

  • 定义形式

存储类型 数据类型 (*函数指针名)();

  • 含义

函数指针指向的是程序代码存储区

提示:对函数指针的定义在形式上比较复杂,如果在程序中出现多个这样的定义,多次重复这样的定义会相当烦琐,一个很好的解决办法是使用 typedef。注意其与一般typedef定义的区别
与其它数据类型不一样,其它格式: typedef 数据类型 变量名 新别名
而函数指针取别名格式为:
typedef <数据类型> (*新别名)(参数)
typedef int (*DoubleIntFunction)(double);
这声明了DoubleIntFunction 为“有一个double 形参、返回类型为 int的函数的指针”类型的别名。下面,需要声明这一类型的变量时,可以直接使用:
DoubleIntFunction funcPtr;
这声明了一个具有该类型的名称为 funcPtr 的函数指针。用 typedef 可以很方便地为复杂类型起别名。

函数指针的典型用途——实现函数回调

  • 通过函数指针调用的函数

    • 例如将函数的指针作为参数传递给一个函数,使得在处理相似事件的时候可以灵活的使用不同的方法。
  • 调用者不关心谁是被调用者

    • 需知道存在一个具有特定原型和限制条件的被调用函数。

函数指针举例

编写一个计算函数compute,对两个整数进行各种计算。有一个形参为指向具体算法函数的指针,根据不同的实参函数,用不同的算法进行计算

编写三个函数:求两个整数的最大值、最小值、和。分别用这三个函数作为实参,测试compute函数

#include <iostream>using namespace std;int compute(int a, int b, int (*func)(int, int)) { return func(a, b);
}int max(int a, int b) {// 求最大值return ((a > b) ? a: b);
}int min(int a, int b) {// 求最小值{return ((a < b) ? a: b);
}int sum(int a, int b) {// 求和return a + b;
}int main() {int a, b, res;cout << "请输入整数a:"; cin >> a;
cout << "请输入整数b:"; cin >> b;res = compute(a, b, &max);cout << "Max of " << a << " and " << b << " is " << res << endl;res = compute(a, b, &min);cout << "Min of " << a << " and " << b << " is " << res << endl;res = compute(a, b, &sum);cout << "Sum of " << a << " and " << b << " is " << res << endl;}

对象指针

对象指针定义形式

类名 *对象指针名;

例:

Point a(5,10);
Point* ptr;
ptr=&a;

通过指针访问对象成员

对象指针名->成员名

例:ptr->getx();相当于 (*ptr).getx();

this指针

指向当前对象自己

隐含于类的每一个非静态成员函数中。

指出成员函数所操作的对象。

当通过一个对象调用成员函数时,系统先将该对象的地址赋给this指针,然后调用成员函数,成员函数对对象的数据成员进行操作时,就隐含使用了this指针。

例如:Point类的getX函数中的语句:

return x;

相当于:

return this->x;

指向类的静态&非静态成员指针

6.2.11节。这个常用吗?


动态内存分配

动态申请内存操作符 new

new 类型名T(初始化参数列表)

功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以初值。

结果值:成功:T类型的指针,指向新分配的内存;失败:抛出异常。

释放内存操作符delete

delete 指针p

功能:释放指针p所指向的内存。p必须是new操作的返回值。

分配和释放动态数组

  • 分配:new 类型名T[数组长度]
    数组长度可以是任何整数类型表达式,在运行时计算释放。
  • 释放:delete[] 数组名p
    释放指针p所指向的数组。p必须是用new分配得到的数组首地址。

new T 和 new T()区别

用 new 建立一个类的对象时,如果该类存在用户定义的默认构造函数,则“new T ”和“new T()”这两种写法的效果是相同的,都会调用这个默认构造函数。但若用户未定义默认构造函数,使用“new T ”创建对象时,会调用系统生成的隐含的默认构造函数;使用“new T()”创建对象时,系统除了执行默认构造函数会执行的那些操作外,还会为基本数据类型和指针类型的成员用 0 赋初值,而且这一过程是递归的。即如果该对象的某个成员对象也没有用户定义的默认构造函数,那么对该成员对象的基本数据类型和指针类型的成员,同样会被以 0 赋初值。

assert检查数组越界

通过C++的assert来进行。assert的含义是“断言”,它是标准 C++的 cassert头文件中定义的一个宏,用来判断一个条件表达式的值是否为 true,如果不为 true,则程序会中止,并且报告出错误,这样就很容易将错误定位。而且仅在debug模式下assert才会其作用。

#include <cassert>
...//获得下标为index的数组元素
Point& element(int index) {assert(index >=0 && index < size); //如果数组下标越界,程序中止return points[index]; //points = new Point[size]
}
//ArrayofPoints points(count)
//ArrayofPoints 私有成员为 Point* points 及 int size
//其引用格式为points.element(index).成员函数

动态创建多维数组

分配:new 类型名T[第一维长度][第二维长度]...
如果内存分配成功,返回一个指向新分配内存首地址的指针。

例如:

char (*fp)[3]; //指向行的指针,是指向数组的指针
fp = new char[2][3];//这里fp可以当成是a[2][3]中的a地址
fp+1指向数组第二行首地址

注意:
char* p[4]
我们可以将其看成 (char*) p[4],这样可以看到p是和[4]在一起的,也就是p[4]是个数组,p存放的是数组中首个元素的地址,数组里存放的是 char类型的数据,char 类型即是指针类型,也就是里面是指针,即:某个值的地址。

char (*p)[4]
我们可以将 (*p)看成一个整体,然后 (*p)存放的是char[4]数组中首个元素的地址,p存放的是(*p)的地址,即:数组的地址。

智能指针

显式管理内存在是能上有优势,但容易出错。

C++11提供智能指针的数据类型,对垃圾回收技术提供了一些支持,实现一定程度的内存管理

C++11的智能指针

unique_ptr :不允许多个指针共享资源,可以用标准库中的move函数转移指针

shared_ptr :多个指针共享资源

weak_ptr :可复制shared_ptr,但其构造或者释放对资源不产生影响

vector对象

为什么需要vector?

  • 封装任何类型的动态数组,自动创建和删除。

  • 数组下标越界检查。

vector对象的定义

vector<元素类型> 数组对象名(数组长度);

例:

vector<int> arr(5)
建立大小为5的int数组

vector对象的使用

  • 对数组元素的引用

与普通数组具有相同形式:

vector对象名 [下标表达式]

vector数组对象的名字表示的就是一个数组对象,而非数组的首地址,因为数组对象不是数组,而是封装了数组的对象。

  • 获得数组长度

size函数

数组对象名.size()

vector举例:

#include <iostream>
#include <vector>
using namespace std;//计算数组arr中元素的平均值double average(const vector<double> &arr) {double sum = 0;for (unsigned i = 0; i<arr.size(); i++)sum += arr[i];return sum / arr.size();
}int main() {unsigned n;cout << "n = ";cin >> n;vector<double> arr(n); //创建数组对象cout << "Please input " << n << " real numbers:" << endl;for (unsigned i = 0; i < n; i++)cin >> arr[i];cout << "Average = " << average(arr) << endl;return 0;
}//基于范围的for循环配合auto举例
#include <vector>
#include <iostream>int main() {std::vector<int> v = {1,2,3};for(auto i = v.begin(); i != v.end(); ++i)std::cout << *i << std::endl;for(auto e : v)std::cout << e << std::endl;
}

深层复制与浅层复制

  • 浅层复制

实现对象间数据元素的一一对应复制。

  • 深层复制

当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指对象进行复制。

浅层复制

深层复制

//深拷贝需new申请内存空间
ArrayOfPoints::ArrayOfPoints(const ArrayOfPoints& v) {size = v.size;points = new Point[size];for (int i = 0; i < size; i++) {points[i] = v.points[i];}
}

移动构造

在现实中有很多这样的例子,我们将钱从一个账号转移到另一个账号,将手机SIM卡转移到另一台手机,将文件从一个位置剪切到另一个位置……移动构造可以减少不必要的复制,带来性能上的提升。

C++11标准中提供了一种新的构造方法——移动构造。

C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。在某些情况下,我们没有必要复制对象——只需要移动它们。

C++11引入移动语义:

源对象资源的控制权全部交给目标对象

临时对象在被复制后,就不再被利用了。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制操作。

什么时候该触发移动构造?

有可被利用的临时对象

移动构造函数:

class_name ( class_name && )

使用移动构造函数

将要返回的局部对象转移到主调函数,省去了构造和删除临时对象的过程

#include<iostream>
using namespace std;
class IntNum {public:
IntNum(int x = 0) : xptr(new int(x)){ //构造函数cout << "Calling constructor..." << endl;
}IntNum(const IntNum & n) : xptr(new int(*n.xptr)) {//复制构造函数cout << "Calling copy constructor..." << endl;
}/*注:
•&&是右值引用
•函数返回的临时变量是右值*/IntNum(IntNum && n): xptr( n.xptr) { //移动构造函数n.xptr = nullptr;cout << "Calling move constructor..." << endl;
}~IntNum() { //析构函数delete xptr;cout << "Destructing..." << endl;
}private:int *xptr;
};//返回值为IntNum类对象
IntNum getNum() {IntNum a;return a;
}int main() {cout << getNum().getInt() << endl; return 0;
}运行结果:Calling constructor...
Calling move constructor...
Destructing...
0
Destructing...

字符串

C风格字符串

  • 字符串常量

例:“program”

各字符连续、顺序存放,每个字符占一个字节,以‘\0’结尾,相当于一个隐含创建的字符常量数组

“program”出现在表达式中,表示这一char数组的首地址

首地址可以赋给char常量指针

const char *STRING1 = "program";

  • 用字符数组存储字符串(C风格字符串)

例如

char str[8] = { ‘p’, ‘r’, ‘o’, ‘g’, ‘r’, ‘a’, ‘m’, ‘\0’ };

char str[8] = “program”;

char str[] = “program”;

string类

//以下均只是示意,并非标准库中就是这么定义的
string();//默认构造函数,建立一个长度为0的串
string(const strings& rhs);//复制构造函数
string(const char* s);//用指针s所指向的字符串常量初始化string类的对象

由于 string 类具有接收 const char*类型的构造函数,因此字符串常量和用字符数组表示的字符串变量都可以隐含地转换为 string 对象(请读者回顾 4.7.2 节有关类类型转换的介绍)。例如,可以直接使用字符串常量对 string 对象初始化:
string str="Hello world!";

学堂在线CPP笔记上(1-6章)相关推荐

  1. 学堂在线《工程伦理》第九章课后习题及答案(仅供参考)

    学堂在线<工程伦理>第八章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十一章课后习题及答案(仅供参考) ...

  2. 1553B学堂在线课程笔记

    课程是学堂在线的 文章目录 带你走进1553B总线-学堂在线 1.机载总线网络概述 2.1553B总线的前世今生 3.1553B总线网络拓扑结构 4.1553B总线的基本组件(物理组成) 5.1553 ...

  3. 学堂在线疾风计划程序设计基础第1-4章

    学堂在线疾风计划程序设计基础 第一章 编程初步 牛刀小试 第二章 变量与代数思维 牛刀小试 逻辑推理与枚举解题 牛刀小试 运行没问题但是提交有问题,有知道原因的么,代码如下? 第四章 筛法与查找 牛刀 ...

  4. 学堂在线《工程伦理》第十二章课后习题及答案(仅供参考)

    学堂在线<工程伦理>第八章课后习题及答案(仅供参考) 学堂在线<工程伦理>第九章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十章课后习题及答案(仅供参考) 学 ...

  5. 学堂在线《工程伦理》第十一章课后习题及答案(仅供参考)

    学堂在线<工程伦理>第八章课后习题及答案(仅供参考) 学堂在线<工程伦理>第九章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十章课后习题及答案(仅供参考) 学 ...

  6. 学堂在线《工程伦理》第八章课后习题及答案(仅供参考)

    学堂在线<工程伦理>第九章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十一章课后习题及答案(仅供参考) ...

  7. 学堂在线《工程伦理》第十章课后习题及答案(仅供参考)

    学堂在线<工程伦理>第八章课后习题及答案(仅供参考) 学堂在线<工程伦理>第九章课后习题及答案(仅供参考) 学堂在线<工程伦理>第十一章课后习题及答案(仅供参考) ...

  8. 学堂在线部分网课笔记---Web设计与应用

    学堂在线部分网课笔记 Web设计与应用 第六章 敏捷的前端框架 6.2.1 bootstrap响应式布局(一) 容器 非固定宽度 固定宽度 Viewport viewport的作用是什么? width ...

  9. 学堂在线_大数据机器学习_小笔记

    学堂在线大数据机器学习小笔记 20220607 - https://www.xuetangx.com/learn/THU08091001026/THU08091001026/10333105/vide ...

  10. 一文弄懂元学习 (Meta Learing)(附代码实战)《繁凡的深度学习笔记》第 15 章 元学习详解 (上)万字中文综述

    <繁凡的深度学习笔记>第 15 章 元学习详解 (上)万字中文综述(DL笔记整理系列) 3043331995@qq.com https://fanfansann.blog.csdn.net ...

最新文章

  1. 嘿嘿 刚刚进来 记录下
  2. xcode 学习笔记2:动态添加view
  3. idea提交git差件_多人合作使用git,推送代码、和并分支
  4. Git 的安装与初次使用 —— Git 学习笔记 03
  5. hdu5299 Circles Game
  6. python基础技巧总结(二)
  7. 乐高创意机器人moc_乐高MOC作品欣赏:变形金刚及其他
  8. 【英语学习】【WOTD】charisma 释义/词源/示例
  9. 电脑护眼设置_99%的人一直坚持着错误的护眼方式!
  10. 便捷注册live、MSN邮箱
  11. Line-in和Mic-in及Line-out的使用和介绍
  12. Postfix权威指南阅读笔记
  13. Java+PDFBox将PDF转成图片
  14. 百度实习生招聘笔试题1
  15. Python 环境及开发工具 IDLE 安装教程
  16. c语言有多难?一个新手刚学c语言的无奈
  17. CKEditor5 富文本编辑器安装以及使用
  18. linux笔记本安装双显卡驱动(intel+nvidia)
  19. 随机森林的特征重要性原理
  20. oracle分段统计总数,Oracle 分段 统计 查询

热门文章

  1. 计算机相关的oa资源,计算机类OA期刊搜集与分析.pdf
  2. 标签制作软件如何制作圆形标签中的弧形文字
  3. python爬取豆瓣电影250_利用Python爬取豆瓣TOP250的电影
  4. AWK手册(ZYF译)
  5. 少年群侠传服务器维护时间,少年群侠传开服表
  6. 微信隐藏代码功能合集
  7. c语言编程温度转换源,c语言编程,将华氏温度转换成摄氏温度。转换公式为:c=5/9 * (f-32),其中f代表华氏温度...
  8. 用C++完成华氏温度换摄氏温度
  9. python改变图片透明度_Python PIL.Image之修改图片背景为透明
  10. 终端安全检测和防御技术