从今天开始就来到了有关C++的学习了,让我们稳步前行把!
我们知道C语言是面向过程的程序化、模块化的语言,适合处理较小规模的程序。对于复杂的问题、规模较大的程序、需要高度的抽象和建模时使用C语言则不是很合适。 20世纪80年代, 计算机界提出了OOP(
object oriented programming:面向对象
)思想,支持面向对象的程序设计语言也应运而生。 1982年,Bjarne Stroustrup(本贾尼)博士在C语言的基础上引入并扩充了面向对象的概念,发明了一 种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。 C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C withclasses | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
---|---|
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++20 | 自C++11以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等 |
C++语言开始时是为了弥补C语言本身和其在面向对象程序设计时的不足。 C++语言是兼容C语言的,所以C++的编译器可以兼容编译C语言所写的程序。 C++语言相比C语言实现了更高层次的封装,是更高级的语言。
C++相比C语言(32个)引入了更多的(63个)关键字,这一点也可以管中窥豹看出一点C++的复杂。
asm | do | if | return | try | continue |
---|---|---|---|---|---|
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
先来看看一个有问题的C语言程序:
#include <stdio.h>
int a = 0;
int a = 0;
int main() {
printf("%d\n", a);
return 0;
}
定义了两个全局同名变量
a
,编译该程序将出现错误。
这与作用域有关系: 对于C语言在同一作用域中不能定义同名的多个变量。
我们虽然不会故意去写上面的那种代码,但是在与他人的协作中出现变量同名现象却也是可能发生的。 C++为了解决C语言所面临的的类似这样的名字冲突问题,引入了命名空间的概念。
每一个命名空间都是一个新的独立封闭的作用域,是C++对C语言中作用域的扩展。 命名空间关键字:
namespace
创建一个命名空间:使用namespace
关键字,后面跟命名空间的名字,最后用{}
包含命名空间的成员。namespace tag{ .... }
#include <iostream>
namespace N {
int a = 0;
int Add(int a, int b) {
return a + b;
}
struct ListNode {
int data;
struct ListNode* next;
};
}
int main() {
int a = 1;
std::cout << N::a << std::endl;
std::cout << N::Add << std::endl;
return 0;
}
于是,我们有了解决办法:把定义的全局变量放入不同的命名空间域中
#include <iostream>
namespace N1 {
int a = 0;
}
namespace N2 {
int a = 0;
}
int main() {
return 0;
}
那么我们应该如何访问到命名空间中的变量、函数、类等我们所定义的成员呢?
直接访问肯定是行不通的,因为命名空间是封闭的,或者说命名空间会影响编译器对标识符的查找规则,导致编译器默认不会到命名空间域中查找。
编译器查找标识符规则: 默认情况:先在标识符所在作用域向上查找,找不到再去全局作用域向上查找,都找不到就报错; 指定(特定)查找:需要使用域作用限定符
::
,直接去指定的作用域去查找标识符,找不到直接报错。
::
scope::tag
两个操作数:左操作数是指定的一个域,右操作数是一个标识符(变量、函数名等) 使用域作用限定符访问命名空间中的成员**:**
#include <iostream>
namespace N1 {
int a = 0;
}
namespace N2 {
int a = 0;
}
int main() {
printf("%d\n", N1::a);
printf("%d\n", N2::a);
return 0;
}
于是,多使用命名空间就很好的解决了名字冲突的问题。
即一个命名空间可以包含另一个命名空间或另外几个命名空间。
//命名空间的嵌套
#include <iostream>
namespace N1 {
int a = 0;
int Add(int a, int b) {
return a + b;
}
namespace N2 {
int a = 0;
int Add(int x, int y) {
return x + y;
}
}
}
int main() {
int rand = 1;
std::cout << &N1::a << std::endl;
std::cout << &N1::N2::a << std::endl;
std::cout << N1::Add << std::endl;
std::cout << N1::N2::Add << std::endl;
return 0;
}
在编译后同名命名空间的成员将会合并到一个命名空间里。
#include <iostream>
namespace N1 {
int a = 0;
int Add(int a, int b) {
return a + b;
}
}
namespace N1 {
int b = 0;
int Sub(int a, int b) {
return a - b;
}
}
int main() {
return 0;
}
注意,合并前的同名命名空间成员不能同名,否则在合并后的同一个命名空间域中就有了同名成员,即重定义,程序将会报错。
#include <iostream>
namespace N1 {
int a = 0;
int Add(int a, int b) {
return a + b;
}
}
namespace N1 {
int a = 0;
int Sub(int a, int b) {
return a - b;
}
}
int main() {
return 0;
}
前面已经介绍了使用域作用限定符访问命名空间中成员的方法,接下来我们来看看另外两种访问命名空间成员的方法吧!
在介绍之前先来了解一下C++官方库定义的命名空间std
C++中的头文件定义的所有内容成员(定义与实现)都处在一个命名空间(作用)域std
中,用以与用户使用的成员相隔离。
我们想要使用头文件的成员时不仅需要包含相应的头文件(预处理时该头文件将会在包含位置处全部展开),还需要再进一步去到命名空间std
中寻找所需要的成员。
又称为命名空间的部分展开;
//命名空间 - 部分展开
#include <iostream>
using std::cout;
using std::endl;
int main() {
cout << "hello!" << endl;
std::cout << "hello!" << std::endl;
return 0;
}
又称为命名空间的全部展开
#include <iostream>
using namespace std;//
int main() {
int a;
double d;
char c;
cin >> a >> d >> c;
cout <<"int: " << a << " " << "double: " << d <<
" " << " char: " << c << " " << endl;
return 0;
}
C++语言本身并没有输入输出语句,实际上C++的输入输出是通过函数调用实现的,这一点与C语言相似。 我们先来看一个输入输出的例子:
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main(){
cout<<"Hello world!"<<endl;
return 0;
}
cout
(读作see out
)和cin
(读作see in
)分别是ostream
和istream
类型的对象,使用cout标准输出对象
(默认绑定控制台)和cin标准输入对象
(默认绑定键盘)时,必须包含< iostream >头文件
以及按命名空间使用方法使用std
。cout
和cin
是全局的流对象,endl
是特殊的C++
符号,表示换行输出,他们都包含在包含< iostream >
头文件中。<<
是流插入运算符(输出运算符),>>
是流提取运算符(输入运算符),>>和<<涉及运算符重载。C++
的输入输出可以自动识别变量类型,非常方便,不需要像printf/scanf
输入输出时那样,需要手动控制格式。当然可以两种输入输出都使用,C++输入输出
某些情况可能不会很方便:如控制浮点数小数点位数、左右对齐等,这时printf、scanf
反而会是更好的选择,我们用着顺手才是最好的。注意:早期标准库将所有功能(定义)在全局域中实现,声明在
.h后缀的头文件中
,使用时只需包含对应 头文件即可,后来将其实现在std命名空间下
,为了和C头文件
区分,也为了正确使用命名空间, 规定C++头文件不带.h
; 旧编译器(如vc 6.0)中还支持<iostream.h>
格式,后续编译器已不支持,因此推荐使用<iostream>+std
的方式 。
std命名空间的使用惯例:
日常练习中直接使用using namespace std;
完全展开std
即可;
实际开发中指定命名空间中特定成员展开即可,如:using std::cout;
、using std::endl;
cerr(读作see error) | 与 标准错误关联 | ostream对象 |
---|---|---|
clog(读作see log) | 与标准错误关联,报告程序的执行信息 | ostream对象 |
在了解缺省参数前,我们先来看看一个简单的普通函数其打印一个整数到控制台console
:
#include <iostream>
using namespace std;
void function(int val) {
cout << val << endl;
}
int main() {
function();//函数需要接受一个int型参数,否则报错
function(10);
return 0;
}
程序报错了,这是C语言的不足之处:无法灵活接收参数,很容易写出僵硬的代码。 C++对此进行了有效的改进:即缺省参数的引入。
缺省参数即函数默认形参参数,在定义或声明函数时,其形参可以直接给出形参合适的缺省(默认)值;在调用含有缺省参数的函数时,调用者就可以选择对缺省参数是否进行实参的传入了,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参 。也就是说,虽然是同一个函数,却出现了不同的调用形式。
#include <iostream>
void function(int val = 0) {
printf("%d\n", val);
}
int main() {
function();
function(10);
return 0;
}
形参从右向左可以连续缺省;不从右开始或有跳跃则报错。
正确举例:
错误举例:
实参可以从左向右连续传入;不从左向右或·有跳跃则出错。
正确举例:
错误举例:
即定义或声明的函数形参都是有缺省值(初始值、默认值)的。
#include <iostream>
using namespace std;
void Func(int a = 0, int b = 0, int c = 0) {
cout << "a " << a << endl;
cout << "b " << b << endl;
cout << "c " << c << endl;
}
int main() {
Func();
Func(100);
Func(100, 200);
Func(100, 200, 300);
return 0;
}
即函数定义或声明中部分形参有缺省值(初始值、默认值)
#include <iostream>
using namespace std;
void Func(int a, int b = 0, int c = 0) {
cout << "a " << a << endl;
cout << "b " << b << endl;
cout << "c " << c << endl;
}
int main() {
Func(100);
Func(100, 200);
Func(100, 200, 300);
return 0;
}
缺省参数不能在函数声明和定义中同时出现 。 也就是说,想要告诉编译器是缺省函数只需要告诉一次就可以了(声明或定义,而不是声明和定义)。
一般来说,缺省参数出现在函数声明中即可(也就是头文件中)。 如果在声明和定义中都出现缺省参数,缺省参数可能会不一致导致二义性。
一上来看到重载也许你会疑问,重载是什么含义?
重载可以理解为一词多义,这一点我们应该不会陌生。
我们先来看一个有问题的C语言程序,全局域中有两个同名函数名Add
,虽然他们的参数类型不同、返回值也不同:
#include <stdio.h>
int Add(int a, int b) {
return a + b;
}
double Add(double x, double y) {
return x + y;
}
int main() {
return 0;
}
C语言中并不支持在同一个作用域中出现同名的函数,但是C++在满足一些条件时可以支持。 函数重载也是C++对C语言的有关函数调用时改进。
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这 些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
//函数重载 - 一词多义
#include <iostream>
using namespace std;
int add(int a, int b) {
return a + b;
}
double add(double x, double y) {
return x + y;
}
int main() {
cout << add(10, 20) << endl;
cout << add(10.2, 20.3) << endl;
return 0;
}
#include <iostream>
using namespace std;
void func() {
cout << "func()" << endl;
}
void func(int a) {
cout << "func(int a)" << endl;
}
int main() {
func();
func(10);
return 0;
}
#include <iostream>
using namespace std;
void func(int a) {
cout << "func(int a)" << endl;
}
void func(double a) {
cout << "func(doublea)" << endl;
}
int main() {
func(10);
func(3.14);
return 0;
}
#include <iostream>
using namespace std;
void func(int a, char b) {
cout << "func(int a, char b)" << endl;
}
void func(char a, int b) {
cout << "func(char a, int b)" << endl;
}
int main() {
func(1, 'a');
func('a', 10);
return 0;
}
先在我们知道C语言不支持重载,而C++支持,那么为什呢?C语言为什么不支持?C++又做了哪些改进从而支持了重载呢?
这一切的一切需要从程序运行前的编译和链接说起。
我们知道一个C/C++程序从源文件到可执行程序需要经过编译和链接阶段,而编译又可以细分为预处理、编译、汇编,也就是预处理、编译、汇编、链接
四个阶段。
让我们再来回顾一下这四个过程,以便于接下来对重载的说明:
windows环境下 对于由多个头文件和多个源文件构成的工程,不同源文件分别经过编译器编译,生成多个目标文件
.o结尾
,这多个目标文件再经过链接生成可执行程序.exe结尾
。 我们需要着重关注的是汇编阶段符号表是如何形成的: 符号表里是编译期间汇总的的全局行变量,包括全局变量、函数名等,同时为这些变量分配一个地址(可能有效也可能无效)。 对于C语言来说,被汇总的函数名并没有进行任何修饰,只是把函数名本身汇总了。这里就是C语言不支持重载的原因: 在同一个源文件中定义的相同的函数名后,到形成符号表这里会出现两个相同的函数名,并且这两个函数还都是有效的,是编译错误。
如果不在同一个源文件中编译链接,分别形成的符号表里各自出现函数名,并且分配一个有效的地址。在接下来的链接阶段,会进行不同符号表的不同符号的合并,此时还是会遇到两个完全相同的函数名并且都是有有效地址,导致链接错误。
以linux下gcc编译器(C语言编译器)的函数名修饰
为例进行说明
在
linux
下,采用gcc
编译完成后,函数名字的修饰没有发生改变,所以同名函数无法区分,也就无法支持重载。
而C++是怎么做的呢? C++形成汇编阶段也会形成符号表,对于函数来说,不是简单的直接把函数名放入符号表,而是对函数名进行事先确定好的规则进行修饰,成为一个独一份的名字以此确保不会与其他有函数定义的函数名重复。
函数名修饰规则与具体的编译器有关,不同的编译器具体实现也不一样,
接下来以linux下g++编译器的函数名修饰规则
为例进行说明:
在
linux
下,采用g++
编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中,只要函数参数不同,修饰出来的名字也就不同,可以区分同名函数,支持了重载 。
前缀_Z
+** 函数名长度** + 每个参数类型的首字母
int Add(int a, int b) {
return a + b;
}
_Z3Addii
void func(int a, double b, int *p) {
printf("%d %.2lf %p\n", a, b, p);
}
_ZFuncidPi
windows下名字修饰约定相比Linux来说比较复杂。
我们前面一直在关注函数名和函数参数的信息,往往忽视了函数返回值类型的信息。 这里牵扯出了两个同名函数,参数个数、类型都相同,只有返回值类型不同,那么这两个函数构成重载吗?
只有返回值类型不同,不构成重载,因为在函数调用时无法区分要调用哪一个函数,产生了二义性。
那么,有一个问题:C++中函数返回值类型可以用来修饰函数名吗?
假设函数返回值类型可以用来修饰函数名 这样经过了名字修饰后也确实会形成新的独一份的名字,但是对于只有返回值不同的两个函数来说,调用反而是一个问题:即无法让编译器知道调用了哪一个函数。
如果构成重载,那么能够真正在程序中使用吗?
不能,原因见前两问。
本节主要介绍了C++
中的命名空间、缺省参数、重载。希望能够帮助到大家。
我们下次再见。