C++对C的加强
1. C++命名空间基本常识
所谓namespace,是指标识符的各种可见范围。C++标准库中的所有标识符都被定义于一个名为std的namespace中。
- 文件<iostream> 和 <iostream.h>格式不一样,前者没有后缀,实际上,在你的编译器include文件夹中可以看到,二者是两个文件,打开文件就会发现,里面的代码是不一样的。后缀为.h的头文件C++标准已经明确提出不支持了, 早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里,C++标准为了和c区别开,也为了正确使用命名空间,规定头文件不使用带后缀.h。因此:
- 1.1 当使用<iostream.h>时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;
- 1.2 当使用<iostream>时,该头文件没有定义全局命名空间,必须使用namespace std; 这样债能正确使用cout;
- 由于namespace的概念,使用C++标准库的任何标识符时,可以有以下三种选择:
- 2.1 直接指定标识符。例如std::ostream 而不是ostream。完整语句如下:
std::cout << std:hex << 3.4 << std::endl;
- 2.2 使用关键字。using std::cout; using std::endl; using std::cin; 以上程序可以写成
using std::cout;
using std::endl;
using std::cin;
cout << std::hex << 3.4 << endl;
- 2.3 最方便就是using namespace std; 例如:using namespace std; 这样命名空间std内定义的所有标志符都有效(曝光)。就好像他们被声明为全局变量一样。那么以上语句可以如下写:
using namespace std;
cout << hex << 3.4 <<endl;
因为标准库非常庞大,所以程序员在选择的类的名称或者函数名时,就有可能和标准库的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都放在名字空间std中。
但是,这样又会带来一个新问题:无数原有的C++代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。所以就有了<iostream.h> 和 <iostream>等等这样的文件,一个是为了兼容以前的C++代码,一个是为了支持新的标准。 命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加".h"。
总之,标准C++引入关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。
既然提高的命名空间这个词,不妨把C语言的命名空间和C++的命名空间两者做一对比,就会更容易接受C++新标准这么指定使用规则的缘由:
C里面的命名空间:
- 在C语言中只有一个全局作用域;
- C语言中所有的全局标识符共享同一个作用域;
- 标识符之间可能发生冲突。
C++提出了命名空间的概念:
- 命名空间将全局作用域分成不同的部分;
- 不同命名空间的标识符可以同名而不会发生冲突;
- 命名空间可以相互嵌套;
- 全局作用域也叫默认命名空间。
案例代码:
#include "iostream"
// 文件iostream中没有引入标准的std,所以需要我们程序员手工写
using namespace std;
namespace namespaceA
{
int a = 10;
}
namespace namespaceB
{
int a = 20;
// namespace 也支持嵌套
namespace namespaceC
{
struct Teacher
{
char name[32];
int age;
} ;
}
}
int main()
{
cout << "namespace test" << endl;
/* using namespace namespaceA; */
cout << namespaceA::a << endl;
cout << namespaceB::a << endl;
using namespaceB::namespaceC::Teacher;
Teacher t1;
t1.age = 23;
}
2.“实用性”加强
C语言的变量都必须在作用于的开始位置定义,但是C++更强调语言“实用性”,所有的变量都可以在需要的时候定义。
3. register关键字增强
在C语言中,
- 1)register关键字请求“编译器”将局部变量存储于寄存器中;
- 2)C语言中无法获得register变量的地址;
在C++语言中, 1)C++编译器有自己的优化方式,即使程序员不使用register也可能做优化, 2)C++中可以获得register变量的地址。 - PS: 所谓的代码优化,就是如果变量出现在循环中被重复的使用,那么编译器在处理代码时就会把变量的存放在寄存器中。
早期的计算机比较慢,编译器不会对代码进行优化,所以使用register来做一个人为地补充。
C++编译器除了有自己的优化代码的方式,在发现程序中需要取register变量的地址的时候,register对变量的声明变得无效。
#include "iostream"
using namespace std;
int main()
{
int nRet = 0;
register int a = 100;
printf("&a = %d\n", &a);
// 上面的语句在C语言中无法编译通过,但是C++可以变异成功
return nRet;
}
4. 变量检测增强
C语言里面的一个“灰色地带”:重复定义多个同名的全局变量是合法的;
C++中编译器不允许定义多个同名的全局变量,会直接拒绝这种二义性的做法。
int g_a;
int g_a = 100;
void main()
{
printf("hello world...\n");
}
5.struct类型增强
C语言不认为预先定义的struct是一种数据类型,所以我使用typedef来自己定义一个数据类型。
C++中就把struct关键字定义的struct直接增强为一种数据类型;
另外还加强的一点是,struct和class一样,可以对自己的元素设定访问权限:public, protected, private。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Teacher
{
char name[64];
int age;
};
struct Student
{
char name[64];
int id;
};
// advTeacher展示了增强的struct和class的相似性,区别以后再表
struct advTecher
{
public:
char name[64];
int id;
protected:
int class;
private:
char age;
};
int main()
{
Teacher t1; //编译器通过
Student s1; //编译器报错
struct Teacher s1; //这样编译器才通过
}
6. 三目运算符增强
在C语言中,运算符的结果本质上就是一个数值,它不是变量,运算符表达式的结果直接保存在寄存器中。如果程序员试图把它当做左值来赋值操作,编译器会报错;
C++中运算符的结果本质上被增强成为一个变量,它保存在内存中,是可以当做左值被修改数值的。
那么这是如何从C语言增强的?
其实就是C++的设计者们改变了运算符的返回对象,把C语言中只是返回一个数值改变为,返回一个变量的地址,代码如下所示:
int a = 10, b = 20;
(a < b ? a : b)
// 改为
(a < b ? &a : &b)
这样一来,尽管运算符返回的仍然是一个数值,但是这个数值是一个内存地址,我们想把它当做左值来进行赋值操作,只需要在地址的前面加上*取地址符即可。
*(a < b ? &a : &b) = 50;
这句话在C语言里面也是可以通过编译并且能够成功执行的。C++的增强实质就是C++编译器帮助程序员们完成了这个操作。
而当运算符是右值的时候,就保持C语言中的处理方式。
7. 对关键字const加强
为了尽可能搞清楚这个话题,我把之前的C语言笔记直接搬回来:
辨析一下const修饰的位置:
const int a;
int const a;
上面这两句话的作用是相同的。
const int *a;
int const *a;
这两句话句话也是同样的作用:指针指向地址的内容不能更改,指针指向的地址可以更改。
int *const a;
const修饰的是a:a指向的地址不可以更改,但是地址里面的内容可以更改。
总结了以下规律:
- 只看const后面的内容,const 后面是个a,a本身是一个指针变量,所以就是指针的值不能改了;
- const后面是*a 或int *a,就是说*a不能改,即a指向的内存的内容不能更改。
int main()
{
const int a; // 整型变量a的值不可修改
int const b; // 同上
const char *c; // 这样的代码按照“从右往左”的顺序开始理解即可避免概念混淆
char *const d; // const char *c = const (char *c): char *c 指向的是一个存储char类型的值的内存,
// 再用const修饰这个内存地址,意思就是:char *c 指向的内存地址不可以被更改,
// 但是指针c可以指向其他的内存地址,指向了新的内存地址后,新的内存的内容也就不能更改。
// 同理,char *const d = char (*const d): *d是一个指针变量(类型不明,指向不明,
// 但是已经为d分配了内存空间)。那么就是d的指向不能改变,d只能一直指向一个固定
// 的内存空间,再用char 来修饰说明,这个d指向的内存空间存储char 类型的值,
// 这个内存空间存储的值当然可以修改的
const char *const e; // 同理,const char *const e = {const [char *(const e)]}:
// e的值不变,e是一个指针,指针e指向一个char类型变量,
// 指针e指向的char变量是不能更改的。
}
下面直接说这个话题的结论:
const关键字在C语言编译器器中并不能真正起到锁定变量值的作用,因为我们只要使用一个变量同类型的指针变量p,用p即可直接修改变量的值;*
但是在C++编译器中真正做到了锁定变量的值,同样的代码,使用p来修改变量值,然后打印结果,发现变量的值不变。*
void main()
{
const int a = 10;
int *p = NULL;
p = (int *)&a;
*p = 9999;
printf("a: %d \n", a);
// C语言编译器处理后运行结果:a: 9999
// C++编译器处理后运行结果:a: 10
}
下面我们来把C++编译器的工作解释一下:
在使用了const关键字修饰变量后,C++编译器会把变量存放在“符号表”中,符号表里面存储的都是键值对:key-value。
注意:这里的“符号表”是和之前的“内存四区”是两个概念,不要拿来试图相互说明。
但是在代码
p = (int *)&a;
中执行的是什么操作呢?编译器会再分配一个新的空间来存放当前a的值,所以p获取的内存地址和a并没有关系,所以C++编译器把const修饰的变量真正变成了一个常量。