0318
C++ Primer Plus - 第九章
第八章 函数探幽
第九章 内存模型和名称空间
9.1 单独编译
大多数C++环境都提供了其他工具来帮助管理,UNIX和Linux系统都具有make程序
,可以跟踪程序以来的文件以及这些文件的最后修改时间。运行make时,如果它检测到上次编译后修改了源文件,make将记住重新构建程序所需的步骤。
头文件 #include "123.h"
原来的一个程序可以分成三部分:
- 头文件:包含结构体声明和使用结构体的函数的原型;
- 源代码文件:包含(与结构体有关的)函数的代码(函数定义);
- 源代码文件:包含调用(与结构体有关的)函数的代码。
头文件中常包含的内容:
- 函数原型;
- 使用#define 或 const定义的符号常量;
- 结构体声明;
- 类声明;
- 模板声明;
- 内联函数。
为什么是双引号,不是尖括号?
在包含头文件时,我们使用"123.h"
,而不是<123.h>
。
因为如果文件名包含在尖括号中,则C++编译器将在(存储标准头文件的)主机系统的文件系统中查找;
但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录。
头文件管理 #ifndef
(if not defined)
在同一个文件中,只能将同一个头文件包含一次,但是很可能在不知情的情况下将头文件包含多次。力图,可能使用包含了另外一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个头文件,它是基于预处理器编译指令#ifndef
的(if not defined)。
头文件coorden.h
:
#ifndef COORDIN_H_
#define COORDIN_H_ //定义名称COORDIN_H_
struct polar{
double distance;
double angle;
};
struct rect{
double x;
double y;
};
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
#endif
编译器首次遇到该文件时,名称COORDIN_H_
没有被定义,编译器将查看#ifndef
和#endif
之间的内容,并读取定义COORDIN_H_
的一行。如果在同一个文件中遇到其它包含coordin.h
的代码,编译器将知道COORDIN_H_
已经被定义了,从而跳到#endif
后面。
注意:这种方法并不能防止编译器将文件包含两次,只是让它忽略除第一次包含之外的所有内容。大多数标准C/C++头文件都是用这种防护方案。
9.2 存储持续性、作用域和链接性
C++笔记3:C++核心编程 --> 1、内存分区模型
C++ Primer Plus(嵌入式公开课)—第4章 复合类型–>4.8.5 自动存储、静态存储和动态存储
9.2.0 存储持续性
- 自动存储持续性:
函数参数
、函数定义中声明的变量(局部变量
) 的存储持续性是自动的。它们在程序开始执行其所属的函数或者代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。栈
- 静态存储持续性:在函数定义外定义的变量(
全局变量
)、使用关键字static定义的变量(静态变量
) 的存储持续性是静态的。它们在程序的整个运行过程中都存在。 - 动态存储持续性:用
new
运算符分配的内存将一直存在,直到使用delete
运算符将其释放或者程序结束为止。这种内存的存储持续性是动态的,被称为自由存储
或堆
。 - 线程存储持续性(C++11):如果变量使用关键字
thread_local
声明,则其生命周期与所属的线程一样长。
9.2.1 作用域与链接
作用域(scope)描述了名称在文件的多大范围内可见:
- 作用域为局部的变量只能在定义它的代码块中使用;—代码块
- 作用域为全局的变量在定义位置到文件结尾之间都可用;—单个文件
- 自动变量的作用域为局部;—代码块
- 静态变量的作用域是全剧还是局部取决于它是如何被定义的;—static
- 在函数原型中定义的名称只在包含参数列表的括号内可用,这就是为什么 这些名称是什么以及是否出现 都不重要的原因;
- 在类中声明的成员的作用域为整个类;
- 在名称空间中声明的变量的作用域为整个名称空间。
链接性(linkage)描述了名称如何在不同单元间共享:
- 链接性为外部的名称可
在文件之间共享
; - 链接性为内部的名称只能由
一个文件中的多个函数共享
; - 自动变量的名称没有链接性,因此他们
不能共享
,只能在它所属的函数或者代码块中使用
。
文件、程序、代码块、函数
代码块:由花括号括起的一系列语句。例如,函数体就是代码块,但可以在函数体中嵌入其他代码块。
文件: 123.h 123.cpp main.cpp 头文件 源程序文件。
程序:一个程序可由多个文件组成。
9.2.2 自动存储持续性(局部变量)
局部变量:函数中声明的函数参数
和变量
。存储在栈
区。
- 存储持续性:自动
- 作用域:局部(函数或代码块内)
- 链接性:没有链接性
C++11 中的 auto
和 register
见9.2.7 说明符和限定符。
局部变量存储在栈区中
程序使用两个指针来跟踪栈:
- 一个指针指向
栈底
—栈的起始位置; - 另一个指针指向
栈顶
—下一个可用内存单元。
当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存;
函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。这些新变量并没有被删除,只是不再被标记,他们所占据的空间将被后来加入到栈中的局部变量所覆盖。
9.2.3 静态持续变量—关键字static
静态变量:函数中声明的函数参数
和变量
。存储在栈
区。
- 存储持续性:静态,在整个程序执行期间一直存在。
- 作用域:多个文件、单个文件、函数或代码块内
- 链接性:外部链接性、内部链接性、没有链接性
未初始化的静态变量的所有位都被设置为0。
外部链接性:可在其他文件中访问;
内部连接性:只能在当前文件中访问;
无链接性:只能在当前函数或代码块中访问。
创建链接性为外部的静态持续变量,必须在代码块的外面声明它;
创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static
限定符;
创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static
限定符。
变量名 | 作用域 | 链接性 | 存储持续性 |
---|---|---|---|
global | 多个文件 | 外部 | 静态 |
ont_file | 单个文件(当前文件) | 内部 | 静态 |
count | 代码块/函数 | 无链接性 | 静态 |
llama | 代码块/函数 | 无链接性 | 自动(局部变量) |
变量count 和 llama的区别:(9.2.6 静态局部变量 和 局部变量 的区别)
- 相同点:二者都只能在
funct1()
中使用; - 不同点:即使
funct1()
函数没有被调用,count
也留在内存中;而llama
只有在funct1()
函数被调用时才会为其分配内存,当函数结束时,此内存会被释放。
☆☆☆☆☆总结:常规外部变量、静态外部变量、静态局部变量、局部变量
1.常规外部变量(函数外定义,不加static),全局变量,多个文件
可访问—9.2.4
2.静态外部变量(函数外定义,加static),全局变量,当前文件
可访问;—9.2.5
3.静态局部变量(函数内定义,加static),全局变量,代码块内
有效;—9.2.6
4.局部变量(函数内定义,不加static),局部变量,代码块内
有效。
其中123是静态变量,4是自动变量,还有一个9.2.10的动态内存。
全局变量是静态变量,存储在全局区;局部变量是自动变量,存储在栈区。
变量 | 存储描述 | 作用域 | 链接性 | 存储持续性 | 如何声明 |
---|---|---|---|---|---|
局部变量 | 自动 | 代码块/函数 | 无链接性 | 自动 | 在代码块中 |
局部变量 | 寄存器 | 代码块/函数 | 无链接性 | 自动 | 在代码块中,使用关键字register |
静态(局部)变量 | 静态,无链接性 | 代码块/函数 | 无链接性 | 静态 | 在代码块中,使用关键字static |
常规外部变量 | 静态,外部链接性 | 多个文件 | 外部 | 静态 | 代码块的外面 |
静态(外部)变量 | 静态,内部链接性 | 单个文件(当前文件) | 内部 | 静态 | 代码块的外面,使用关键字static |
9.2.4 静态持续性、外部链接性—关键字extern
单定义规则:变量只能有一次定义。
使用关键字extern
前提是静态变量,具有外部链接性,即在代码块的外面声明的变量。看个示例:
9.5.cpp
//程序清单9.5:
#include<iostream>
using std::cout; using std::endl;
double warming = 0.3;//①链接性为外部的静态变量
void update(double dt); //②
void local(); //③
int main(){
cout << "main warming = " << warming << endl;//0.3
update(0.1);//④
cout << "main warming = " << warming << endl;//0.4
local(); //⑤
cout << "main warming = " << warming << endl;//0.4
return 0;
}
9.6.cpp
//程序清单9.6:
#include<iostream>
using std::cout; using std::endl;
extern double warming;//⑥
void update(double dt);
void local();
void update(double dt){
extern double warming;//⑦这行代码可有可无
warming += dt;
cout << "update warming = " << warming << endl;//0.4
}
void local(){
double warming = 0.8;//⑧
cout << "local warming = " << warming << endl;//0.8
cout << "local ::warming = " << ::warming << endl;//⑨0.4
}
分析:
1.定义一个全局变量warming,链接性为外部,即可被多个文件访问;
2.3.函数声明;—这个不可少
4.5.函数调用;
6.使用关键字extern
声明变量warming
,让该文件中的函数都能够使用它;
7.再次使用关键字extern声明变量warming,这句可有可无,因为已经有⑤了。
8.在local()
函数中定义了一个与全局变量warming同名的局部变量
,因此在此函数中,这个局部变量将隐藏全局变量;但依然可以利用::
来调用全局变量(下面第8点);
9.利用作用域解析运算符::
表示使用变量的全局版本–>9.3 的 局部名称隐藏名称空间名?。
结果:
多文件编译 g++ 9.5.cpp 9.6.cpp
☆☆☆
g++ 9.5.cpp 9.6.cpp
./a.cout
如果只在含有main函数的程序中按F5,就会编译出错或者没结果,因为这样只会编译当前文件,而不会链接到其他文件。
作用域解析运算符::
C++提供了作用域解析运算符::
。放在变量名前面时,该运算符表示使用变量的全局版本
。
总结:
在多文件程序中,可以在一个文件(且只能在一个文件)中定义一个外部变量
—单定义规则。
其他文件要使用该变量,必须使用关键字extern
声明它。
9.2.5 静态持续性、内部链接性
如果文件1中定义了一个常规外部变量errors
(外部链接性),
而文件2中定义了一个同名的静态(外部)变量
(内部链接性),则在文件2中,静态(外部)变量将隐藏常规外部变量。
看个示例:
9.7.cpp
//程序清单9.7:
#include<iostream>
using std::cout; using std::endl;
int tom = 3; //常规外部变量---全局变量,多个文件可访问
int dick = 30; //常规外部变量---全局变量,多个文件可访问
static int harry = 300; //静态外部变量---全局变量,当前文件可访问 = 300
void remote_access();
int main(){
cout << "main: tom = " << tom << "; &tom = " << &tom << endl;
cout << "main: dick = " << dick << "; &dick = " << &dick << endl;
cout << "main: harry = " << harry << "; &harry = " << &harry << endl;
remote_access();
return 0;
}
9.8.cpp
//程序清单9.8:
#include<iostream>
using std::cout; using std::endl;
extern int tom; //使用文件1中定义的常规外部变量---全局变量,多个文件可访问
static int dick = 10; //静态外部变量---全局变量,当前文件可访问,并且会覆盖 = 30
int harry = 200; //常规外部变量---全局变量,多个文件可访问
void remote_access(){
cout << "remote_access: tom = " << tom << "; &tom = " << &tom << endl;//
cout << "remote_access: dick = " << dick << "; &dick = " << &dick << endl;
cout << "remote_access: harry = " << harry << "; &harry = " << &harry << endl;
}
分析:
1.在9.7.cpp中定义并初始化的变量tom
是链接性为外部的静态变量,在9.8.cpp中想用,就需要加个关键字extern
;
2.在9.7.cpp中定义并初始化的变量dick
是链接性为外部的静态变量,在9.8.cpp中想用,就需要加个关键字extern
;但是9.8.cpp中也定义了一个同名的链接性为内部的静态变量,所以在9.8.cpp中静态(外部)变量就覆盖了常规外部变量;
3.同理,在9.8.cpp中定义并初始化的变量harry
是链接性为外部的静态变量,在9.7.cpp中想用,就需要加个关键字extern
;但是9.7.cpp中也定义了一个同名的链接性为内部的静态变量,所以在9.7.cpp中静态(外部)变量就覆盖了常规外部变量。
结果:
结论:
如果有作用域范围更小的变量,就把作用于更大的变量给覆盖了,类似于前面的局部变量覆盖全局变量。
9.2.6 静态存储持续性、无链接性
之前的一个示例:(8.8 编程练习 第1题:static变量记录函数被调用的次数)
#include<iostream>
using std::cout; using std::cin; using std::endl;
void func1();//返回函数被调用的次数
int main(){
func1();
func1();
func1();
func1();
func1();
return 0;
}
void func1(){
static int count = 0;//静态局部变量
int num = 0;//局部变量
num += 2;
cout << "num = " << num << "; ";
count++;
cout << "函数被调用第 count = " << count << "次。" << endl;
}
结果:
静态局部变量 和 局部变量:
自动变量(局部变量
)num 在每次调用函数时都会被重置为0;
而静态局部变量
count 只在程序运行时被设置为0,以后每次调用函数时,它都保留着上一次调用后的结果,因此可以(累加)记录函数被调用的总次数。
9.2.4 9.2.5 9.2.6 总结
9.2.4中:
- 9.5.cpp 和 9.6.cpp 是为了测试
常规外部变量
的使用,即在一个文件定义了一个外部变量(函数外定义、不加static),另一个文件想用,那就要加关键字extern
;最后要多文件编译:g++ 9.5.cpp 9.6.cpp
9.2.5中:
- 9.7.cpp 和 9.8.cpp 是为了测试
静态外部变量
(函数外定义、加static)覆盖常规外部变量
(函数外定义、不加static),最后也要多文件编译:g++ 9.7.cpp 9.8.cpp
9.2.6中:
示例中是比较静态局部变量
(函数内定义、加static)和局部变量
(函数内定义、不加static),这个不需要多文件编程,可以直接按F5
对当前的文件进行编译运行。
9.2.7 说明符和限定符
存储说明符(storage class specifier):
- auto:C++11之前,auto指出变量为自动变量;但在C++11中,auto用于自动类型推断。
- register:用于在声明中指示寄存器存储;而在C++11中,它只是显式地指出变量是自动的。
- static:静态局部变量、静态外部变量(常规外部变量是不加static的那个)。
- extern:常规外部变量,另一个文件中要用时要加extern。
- mutable:下面具体介绍。
cv-限定符(cv-qualifier):
- const:默认情况下全局变量的链接性为外部的(这里应该说的是常规外部变量),但
const + 全局变量
的链接性为内部的,也就是说在函数外定义const int num = 10;
和static int num = 10;
是等效的,都是只在当前文件可用。 - volatile
关键词mutable
可以通过mutable
来指出,即使结构体变量为const,其某个成员也可以被修改。
struct data{
char name[30];
mutable int num;//加了mutable关键词
};
const data veep = {"abc",123};
//strcpy(veep.name, "xyz");//将报错,因为结构体变量veep是const类型
veep.num++;//这个可以,虽然结构体变量veep是const类型,但结构体成员num前加了关键字mutable,因此num可以被修改。
9.2.8 函数和链接性
所有函数的
- 存储持续性:
静态
,在整个程序执行期间都一直存在; - 链接性:默认情况下是
外部
,即可以在文件间共享,一个文件中定义函数,其他文件要使用此函数,前面要加extern
关键字; - 另外还可以使用关键字
static
将函数的链接性设置为内部
,使之只能在一个文件中使用,但必须同时在原型和函数定义中使用该关键字:
static void func1();//函数原型
static void func1(){//函数定义
...
}
9.2.9 语言链接性
9.2.10 存储方案和动态分配
动态内存由运算符new
和delete
控制,而不是由作用域和链接性规则控制。
因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。
通常,编译器使用三块独立的内存:
- 一块用于静态变量(常规外部变量、静态外部变量、静态局部变量);—全局区
- 一块用于自动变量(函数参数、局部变量);—栈区
- 一块用于动态内存(new);—堆区/自由存储区
使用new运算符初始化:
new一个变量:
int* p1 = new int(6);//new一个int型变量,并赋值为6
double* p2 = new double(6.6);//new一个double型变量,并赋值为6.6
new一个结构体和数组:
struct where{double x; double y; double z;};
where* p3 = new where { 2.2, 3.3, 1.1 };//new一个结构体并初始化
int* p4 = new int[5]{1, 2, 3, 4, 5 };//new一个数组并初始化
定位new运算符—头文件#include<new>
通常,new
负责在堆
(heap
)中找到一个足以满足要求的内存块。new运算符还有一种变体,被称为定位new
运算符,它让您能够 指定 要使用的内存块的位置。
要使用定位new
的特性,首先要包含头文件new
。
书写格式对比:
#include<new> //①
char buffer[512];//②用一个静态char数组为定位new运算符提供内存空间,这块内存空间在全局区
const int N = 5;
int main(){
double* p1;
double* p2;
p1 = new double[N]; //常规new
p2 = new(buffer) double[N]; //定位new
...
delete[] p1;//③
return 0;
}
1.包含头文件new
;
2.提前准备好buffer
空间;
3.不用delete[]
,因为定位new指向的buffer空间在全局区/静态区
,而delete只能释放堆区
的内存。
通过一个示例来说明常规new
运算符和定位new
运算符的区别:
#include<iostream>
#include<new>
using std::cout; using std::cin; using std::endl;
char buffer[512];//用一个静态char数组为定位new运算符提供内存空间,这块内存空间在全局区
const int N = 5;
int main(){
double* p1;
double* p2;
p1 = new double[N];
p2 = new(buffer) double[N];
cout << "堆区的地址:" << p1 << "; 全局区的地址:" << (void *)buffer << endl;//这里要写 (void *),否则输出的是字符串的内容
for(int i = 0; i < N; i++){
p1[i] = i + 10.1;
p2[i] = i + 20.2;
}
for(int i = 0; i < N; i++){
cout << "p1[i] = " << p1[i] << "; &p1[i] = " << &p1[i] << " --- ";
cout << "p2[i] = " << p2[i] << "; &p2[i] = " << &p2[i] << endl;
}
delete[] p1;
cout << "delete[] p1;" << endl;
int* p3;
int* p4;
p3 = new int[N];
p4 = new(buffer + sizeof(int)) int[N];
cout << "堆区的地址:" << p3 << "; 全局区的地址:" << (void *)buffer << endl;//这里要写 (void *),否则输出的是字符串的内容
for(int i = 0; i < N; i++){
p3[i] = i + 10;
p4[i] = i + 20;
}
for(int i = 0; i < N; i++){
cout << "p3[i] = " << p3[i] << "; &p3[i] = " << &p3[i] << " --- ";
cout << "p4[i] = " << p4[i] << "; &p4[i] = " << &p4[i] << endl;
}
delete[] p3;
cout << "delete[] p3;" << endl;
return 0;
}
结果:
分析:
- 首先
char buffer[512];
//用一个静态char数组
为定位new运算符
提供内存空间(这块内存空间在全局区),即定位new运算符
将数组p2
放在了数组buffer
中,因此p2的地址和buffer的地址一样。但要注意的是,p2是double指针,buffer是char指针,所以输出buffer
的地址时需要进行强制转换(void *)buffer
,否则将输出字符串的内容。
因此说:
常规new运算符
将数组p1
放在了堆
中;
而定位new运算符
将数组p2
放到了数组buffer
中。 - 结果中
地址1
和2
是常规new
在堆区开辟的空间地址,而这可能一样,也可能不一样,见下面的补充;
地址3
是数组buffer
的地址,地址5
是数组p2
的首元素的地址;
地址4
是数组buffer
的地址,地址6
是数组p4
的首元素的地址;
地址5
和地址6
为什么相差4,是因为p2 = new(buffer) double[N];
//定位new把数组p2放到数组buffer的开始位置,而p4 = new(buffer + sizeof(int)) int[N];
//定位new把数组p4放到数组buffer的开始位置再加一个int型数据的大小,即4字节,所以数组p2和p4的起始位置相差4字节。 常规new
需要配合delete
来释放内存,而定位new不需要
,因为数组buffer实在全局区,属于静态内存,而delete
只能用于指向new
分配的堆
内存,即delete
只能释放堆区的内存。
补充:
int* p1 = new int;//p1的地址
...
delete p1;
p1 = new int;//p1的地址
...
输出p1的地址,结果可能相同,也可能不同(C++第4章 中 4.7.5的补充:几次new的内存地址是一样的 和 4.8.4的案例2又不一样了)。
9.3 名称空间namespace
1.随着项目的增大,名称互相冲突的可能性也将增加,这种冲突被称为名称空间问题。C++提供了名称空间工具,以便更好地控制名称的作用域。
2.一个名称空间中的名称不会和另一个名称空间中的同名名称发生冲突。名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。—>复习题 第5题
3.因此在默认情况下,(在名称空间中声明的)名称的链接性为外部的,即其作用域是全局
。—>上面的9.5.cpp 9.6.cpp 9.7.cpp 9.8.cpp文件中就利用了这点
如何访问给定名称空间中的名称?
最简单的方法是通过作用域解析运算符::
。
9.3.1 using声明 和 using编译指令
(第6章的 6.6 补充:空间std的使用规则)
区别:(复习题第2题)
using声明是一个名称可用,而using编译指令使所有的名称都可用。
先创建一个名称空间Jill
:
void other();//函数原型
namespace Jill{
double fetch;//double型变量
double bucket(double n){ ... }//一个函数定义
struct Hill{ ... };//一个结构体
void pail();//一个函数原型
}
using声明:只让名称fetch
可用(注意避免产生二义性)
#include<iostream>
using Jill::fetch;//当前文件的所有函数都可以用名称fetch
//using Jack::fetch;//这样会产生二义性,因此说不允许同时使用两个using声明一个同名名称
int main(){
fetch++;
other()
}
void other(){//函数定义
cout << fetch;
}
using编译指令:使得名称空间中所有名称都可用
#include<iostream>
using namespace std;//在本文件的所有函数中都可以用名称空间std中的所有名称
int main(){
using namespace Jill;//在main函数中可以使用名称空间Jill中的所有名称
...
}
不用using声明,也不用using编译指令:(这样就不会产生二义性)
#include<iostream>
//using Jill::fetch;//当前文件的所有函数都可以用名称fetch
//using Jack::fetch;//这样会产生二义性,因此说不允许同时使用两个using声明一个同名名称
int main(){
Jill::fetch++;
other()
}
void other(){//函数定义
cout << Jack::fetch;
}
9.3.2 局部名称隐藏名称空间名?
namespace Jill{
double fetch;//double型变量
double bucket(double n){ ... }//一个函数定义
struct Hill{ ... };//一个结构体
void pail();//一个函数原型
}
char fetch;//全局变量名称
int main(){
using namespace Jill;//using编译指令 main函数中可以使用Jill中的所有名称
Hill abc;//创建一个结构体变量
double water = bucket(2.0);//调用bucket函数
double fetch;//局部变量名称fetch
cin >> fetch;//局部变量fetch
cin >> ::fetch;//全局变量fetch
cin >> Jill::fetch;//Jill中的fetch
...
}
int foom(){
//Hill aaa;//会报错,无法直接使用名称空间Jill中的名称Hill,需要用using声明或者using编译指令
Jill::Hill bbb;//ok
}
上述例子中没用到using声明
,只用到了using编译指令
。
在main()
中,名称Jill::fetch
被放在局部名称空间(只能在main函数体/代码块中使用,所以说是局部名称空间)中,但其作用域不是局部的,因此不会覆盖全局的fetch
;
然而局部声明的fetch
将隐藏Jill::fetch
和全局fetch
;
但是依然可以使用作用域解析运算符::
来使用Jill::fetch
和全局的fetch
。
一般来说,使用using声明比使用using编译指令更安全
。
补充:iostream.h 和 iostream 的区别
以下两种写法是等价的:(第六章 补充:空间std的使用规则 ①名称空间std)
#include<iostream>
using namespace std;
#include<iostream.h>
9.3.3 如果要求不能使用 using声明 和 using编译指令,该怎么写?(复习题第3、4题)
1.不使用using声明,使用using编译指令:
using namespace std;
2.使用using声明,不使用using编译指令:
using std::cout; using std::cin; using std::endl;
3.既不使用using声明,也不使用using编译指令:
std::cout std::cin std::endl;
回顾第2章的复习题第11题:
9.3.4 名称空间示例
namesp.h:
//程序清单9.11:
#include<string>
using std::string;
namespace pers{
struct Person
{
string fname;
string lname;
};
void getPerson(Person &);
void showPerson(const Person &);
}
namespace debts{
using namespace pers;
struct Debt
{
Person name;
double amount;
};
void getDebt(Debt &);
void showDebt(const Debt &);
double sumDebts(const Debt ar[], int n);
}
namesp.cpp:
//程序清单9.12:
#include<iostream>
#include "namesp.h"
using std::cout; using std::cin; using std::endl;
namespace pers{
void getPerson(Person &rp){
cout << "请输入姓:";
getline(cin,rp.fname);
cout << "请输入名:";
getline(cin,rp.lname);
}
void showPerson(const Person & rp){
cout << "姓: " << rp.fname << "; 名: " << rp.lname << "; ";
}
}
namespace debts{
void getDebt(Debt & rd){
getPerson(rd.name);
cout << "请输入债务数额:";
cin >> rd.amount;
cin.get();
}
void showDebt(const Debt & rd){
showPerson(rd.name);
cout << "债务数额:" << rd.amount << endl;
}
double sumDebts(const Debt ar[], int n){
double sum = 0;
for(int i = 0; i < n; i++){
sum += ar[i].amount;
}
return sum;
}
}
main.cpp:
//程序清单9.13:
#include<iostream>
#include "namesp.h"
using std::cout; using std::endl;
void other();
void another();
int main(){
using debts::Debt;
using debts::showDebt;
Debt golf = {"Benny", "Goatsniff", 120.0};
showDebt(golf);
other();
another();
return 0;
}
void other(){
using namespace debts;
Person dg = {"Doodles", "Glister"};
showPerson(dg);
cout << endl;
Debt zippy[3];
for(int i = 0; i < 3; i++){
getDebt(zippy[i]);
}
for(int i = 0; i < 3; i++){
showDebt(zippy[i]);
}
cout << "总债务为:" << sumDebts(zippy,3) << endl;
}
void another(){
using pers::Person;
Person collector = {"Milo", "Rightshift"};
pers::showPerson(collector);
cout << endl;
}
结果:
9.4 总结
9.1-9.3
9.5 复习题
第2题:using声明 和 using编译指令 的区别☆☆☆
首先,
using 声明可以单独使用名称空间中某个特定的名称,其作用域与 using 所在的声明区域相同;
而using编译指会使名称空间中的所有名称可用。如果在全局中使用 using编译指令,将使该名称空间中的名称在全局可用;如果在函数定义中使用 using 编译指令,将会在该函数中使该名称空间可用。
此外,在名称冲突时,两者也会有部分差异:
名称空间和using 声明的区域存在相同的名称,如果在该区域中使用 using 声明导入名称,则两个名称会发生冲突而出错;例如:using Jacd::fetch; using Jill::fetch;
//会产生二义性
而如果使用 using 编译指令,则该区域的局部版本
名称将会隐藏名称空间
的版本。–>9.3.2 局部名称隐藏名称空间名?
因此很多情况下认为 using 声明
只导入需要的部分名称,它在使用上比using编译指令要更安全。
第3、4题:不能使用 using声明 和 using编译指令,该怎么写?
上面的9.3.3如果要求不能使用 using声明 和 using编译指令,该怎么写?
第5题:函数的作用域设置为单文件访问
(9.2.8 函数和链接性的内容)
分析:
在同一个程序的不同文件中使用不同函数,且由于两个average()
函数的参数列表相同(即特征标相同),因此不能使用函数重载。
解决方案是:
1.定义不同的名称空间,调用时使用作用域解析符::
;
2.在每个文件中包含单独的静态函数定义,将其限制为内部链接函数,即使用关键字static
将函数的链接性设置为内部
,使之只能在一个文件中使用
,但必须同时在原型和函数定义
中使用关键字static
:
答案:
方法1:
//创建两个名称空间:
namespace ns1{
int average(int m,int n){return (m + n) / 2;}
}
namespace ns2{
double average(int m,int n){return (double)(m + n) / 2.0;}
}
//调用时使用作用域解析符:
int res1 = ns1::average(3,6);
double res2 = ns2::average(3,6);
方法2:
文件1:
stactic int average(int m,int n);
stactic int average(int m,int n){
return (m + n) / 2;
}
文件2:
stactic double average(int m,int n);
stactic double average(int m,int n){
return (double)(m + n) / 2.0;
}
第6题:未初始化的静态变量的所有位都被设置为0。
int x = 10;
int y;
int main(){
cout << x << y << endl;//10 0
return 0;
}
9.6 编程练习
第1题:多文件编译
多文件编程在vscode上总会编译不过,或者编译过了就是不出结果。
答:请教了拓兄,发现是直接按F5只是编译运行当前文件,如果是同时编译运行多个文件,就要在终端通过g++ a.cpp b.cpp ...
然后./a.out
题目不难,就是写起来比较绕。
代码:
golf.h:
const int Len = 40;
struct golf
{
char fullname[Len];
int handicap;
};
void setgolf(golf& g, const char* name, int hc);
int setgolf(golf& g);
void handicap(golf& g, int hc);
void showgolf(const golf& g);
golf.cpp:
#include<iostream>
#include<cstring>
#include "golf.h"
using std::cout; using std::endl;
void setgolf(golf& g, const char* name, int hc){
strcpy(g.fullname,name);
g.handicap = hc;
}
int setgolf(golf& g){
if(strcmp(g.fullname,"") == false)//name是空串,就返回0
return 0;
else
return 1;
// if(!strcmp(g.fullname,""))//name是空串,就返回0
// return 0;
// else
// return 1;
// if(strcmp(g.fullname,""))//name是空串,就返回0
// return 1;
// else
// return 0;
}
void handicap(golf& g, int hc){
g.handicap = hc;
}
void showgolf(const golf& g){
cout << "姓名:" << g.fullname << "; 等级:" << g.handicap << endl;
}
golf_main.cpp:
#include<iostream>
#include<cstring>
#include "golf.h"
using std::cin; using std::cout; using std::endl;
int main(){
golf info[5];
int count = 0;
char name[Len];
int hc;
for(int i = 0; i < 5;i++){
cout << "请输入用户名:";
cin.getline(name,Len);
cout << "请输入用户等级:";
cin >> hc;
cin.get();//清缓存 或者while(cin.get() != '\n');
setgolf(info[i],name,hc);
//showgolf(info[i]);
if(!setgolf(info[i])){
cout << "您输入的姓名为空,输入结束!";
break;
}
count++;
}
if(count == 5)
cout << "输入结束,";
cout << "count = " << count << endl;
for(int i = 0; i < count; i++){
showgolf(info[i]);
}
cout << "调用handicap()函数修改handicap值:" << endl;
handicap(info[0], info[0].handicap + 10);
showgolf(info[0]);
handicap(info[count - 1], info[count - 1].handicap + 10);
showgolf(info[count - 1]);
// golf g1;
// setgolf(g1,"abcde",12);
// showgolf(g1);
// handicap(g1,26);
// showgolf(g1);
// golf g2;
// setgolf(g2,"",20);
// cout << setgolf(g2) << endl;
// showgolf(g2);
return 0;
}
结果:
第3题:定位new运算符
代码1:将一个静态数组用作缓冲区,不用delete,静态数组buffer在全局区/静态区。
#include<iostream>
#include<new>
#include<cstring>
using std::cout; using std::endl;
char buffer[256];//将一个静态数组用作缓冲区
struct Students
{
char name[20];
int age;
};
int main(){
Students* p = new(buffer) Students[2];
strcpy(p[0].name,"marco reus");
p[0].age = 26;
strcpy(p[1].name,"thiago silva");
p[1].age = 29;
for(int i = 0; i < 2; i++){
cout << "姓名:" << p[i].name << ";年龄:" << p[i].age << endl;
}
cout << "buffer的地址:" << (void *)buffer << endl;
cout << "p = " << p << endl;
cout << "&p[0] = " << &p[0] << endl;
cout << "&p[1] = " << &p[1] << endl;
return 0;
}
结果:
代码2:使用常规new运算符来分配缓冲区,要记得delete,因为缓冲区buffer1在堆区。
#include<iostream>
#include<new>
#include<cstring>
using std::cout; using std::endl;
char buffer[256];//将一个静态数组用作缓冲区
struct Students
{
char name[20];
int age;
};
int main(){
char* buffer1 = new char[256];//使用常规new运算符分配缓冲区
//Students* p = new(buffer) Students[2];
Students* p = new(buffer1) Students[2];
strcpy(p[0].name,"marco reus");
p[0].age = 26;
strcpy(p[1].name,"thiago silva");
p[1].age = 29;
for(int i = 0; i < 2; i++){
cout << "姓名:" << p[i].name << ";年龄:" << p[i].age << endl;
}
//cout << "buffer的地址:" << (void *)buffer << endl;
cout << "buffer1的地址:" << (void *)buffer1 << endl;
cout << "p = " << p << endl;
cout << "&p[0] = " << &p[0] << endl;
cout << "&p[1] = " << &p[1] << endl;
delete[] buffer1;
cout << "delete[] buffer1" << endl;
return 0;
}
结果:
第4题:名称空间
代码:
namesp.h
namespace SALES{
const int QUARTERS = 4;
struct Sales
{
double sales[QUARTERS];
double average;
double max;
double min;
};
void setSales(Sales & s, const double ar[], int n);
void setSales(Sales & s);
void showSales(const Sales & s);
}
namesp.cpp
#include<iostream>
#include "namesp.h"
using std::cout; using std::endl; using std::cin;
namespace SALES{
void setSales(Sales & s, const double ar[], int n){
double max,min,ave;
max = min = ar[0];//1.不要赋0,直接赋ar[0]
ave = 0.0;
if(n > QUARTERS){//2.判断n和QUARTER的大小关系
for(int i = 0; i < QUARTERS; i++) s.sales[i] = ar[i];
}
else{
for(int i = 0; i < n; i++) s.sales[i] = ar[i];
for(int i = n; i < QUARTERS; i++) s.sales[i] = 0;//2.其余位置补0
}
for(int i = 0; i < n; i++){
ave += s.sales[i];
if(s.sales[i] > max) max = s.sales[i];
if(s.sales[i] < min) min = s.sales[i];
}
s.max = max;
s.min = min;
s.average = ave / QUARTERS;
}
void setSales(Sales & s){
double arr[QUARTERS] = {};
// double max,min,ave;
// max = min = arr[0];
// ave = 0.0;
for(int i = 0; i < QUARTERS; i++){
cout << "请输入第 " << i+1 << " 个季度的销售额:";
cin >> arr[i];
while(!cin){//3.判断输入的是否是数字
cin.clear();
while(cin.get() != '\n');
cout << "输入错误,请重新输入第 " << i+1 << " 个季度的销售额:";
cin >> arr[i];
}
s.sales[i] = arr[i];
// ave += s.sales[i];
// if(s.sales[i] > max) max = s.sales[i];
// if(s.sales[i] < min) min = s.sales[i];
}
// s.max = max;
// s.min = min;
// s.average = ave / QUARTERS;
setSales(s,arr,QUARTERS);//4.得到arr数组之后,可以直接调用namespace中的重载函数setSales来初始化结构体
}
void showSales(const Sales & s){
cout << "销售额:";
for(int i = 0; i < QUARTERS; i++){
cout << s.sales[i] << "; ";
}
cout << "最大值:" << s.max << "; 最小值:" << s.min << ";平均值:" << s.average << endl;
}
}
main.cpp
#include<iostream>
#include "namesp.h"
using std::cout; using std::endl;
int main(){
using namespace SALES;
Sales s1;
double arr[3] = {11.1, 22.2, 33.3};
setSales(s1,arr,3);
showSales(s1);
Sales s2;
setSales(s2);
showSales(s2);
return 0;
}
分析:
上面是最终的比较完善的代码,相比于自己写的,有几点需要注意:
1.计算max和min时,初始值不要给0,直接给到数组的首元素ar[0];
2.要加个判断 给进来的数组ar[]的元素个数n 和 结构体中元素个数QUARTER 的大小关系。如果n > QUARTERS,就直接赋QUARTER个值到结构体中的数组;反之,就直接赋n个值到结构体中的数组,并且给后面的元素补零
3.输入数字的时候,做如下判断最保险:
cout << "请输入第 " << i+1 << " 个季度的销售额:";
cin >> arr[i];
while(!cin){
cin.clear();//改状态
while(cin.get() != '\n');//清缓存
cout << "输入错误,请重新输入第 " << i+1 << " 个季度的销售额:";
cin >> arr[i];
}
4.可以在void setSales(Sales & s);
中调用void setSales(Sales & s, const double ar[], int n);
,如下:
setSales(s,arr,QUARTERS);//4.得到arr数组之后,可以直接调用namespace中的重载函数setSales来初始化结构体
☆☆☆多文件编译总结:123.h 123.cpp main.cpp
到底要不要都写 #include<iostream>
和 using namespace std;
本章一共有几个地方涉及到这种多文件编程:
①9.2.4:
- 9.5.cpp 和 9.6.cpp 是为了测试常规外部变量的使用,即在一个文件定义了一个外部变量(函数外定义、不加static),另一个文件想用,那就要加关键字
extern
;最后要多文件编译:g++ 9.5.cpp 9.6.cpp
②9.2.5:
- 9.7.cpp 和 9.8.cpp 是为了测试静态外部变量(函数外定义、加static)覆盖常规外部变量(函数外定义、不加static),最后也要多文件编译:
g++ 9.7.cpp 9.8.cpp
③9.3.4 名称空间示例:
- namesp.h namesp.cpp main.cpp
④9.6编程练习第1题:
- golf.h golf.cpp main.cpp
⑤9.6编程练习第4题:
- namesp.h namesp.cpp main.cpp
分析:
①②没有涉及到头文件,只是两个文件之间测试各类静态变量的使用,所以就是各写各的,每个文件都是一个独立的文件,因此都要写
#include<iostream>
和using namespace std;
③④⑤的话涉及到头文件,其中两个.cpp文件就跟①②中的情况一样,都两个独立的文件,不同的是③④⑤中有个.h文件,因此两个.cpp文件就要包含这个头文件,具体写法是#include “namesp.h”,并且在这个头文件中不用写
#include<iostream>
和using namespace std;
,因为头文件中常包含的内容是
也就是说头文件中都是变量声明、函数原型(声明)、结构体声明、类声明、模板声明等,不涉及具体的函数定义这些,如果只涉及到常规的内置类型的数据,就不用写#include<iostream>
和using namespace std;
,但是如果涉及到string
类型的变量,那就要在头文件中加个#include<string> using std::string;
了。
(第9章完)