从C语言到C++:C++入门1
本系列旨在以C语言为基础,介绍C++的基本语法和特性。C语言中的基本语法包括变量、函数、循环、条件语句等,在C++中同样适用。C++在C语言的基础上,新增了一些特性,如类、对象、继承、多态等。
C++相较于C语言,最大的优势在于其对面向对象编程的支持,这使得C++更符合人类的思维模式,更符合实际问题的解决需求。下面,我们将以C语言开发中会遇到的具体问题为导向,逐步介绍C++的其他特性。
C++对于C语言的兼容
C++对于C语言的兼容性是其最大的优势之一。C++可以无缝地与C语言代码混合使用,这使得C++在开发中可以利用C语言的成熟库和工具,同时也可以利用C++的面向对象编程优势。
C++中使用C语言的语法输出"Hello World!":
#include<stdio.h>intmain(){printf("Hello World!\n");return0;}C++中使用C++的语法输出"Hello World!":
#include<iostream>usingnamespacestd;intmain(){cout<<"Hello World!"<<endl;return0;}上面两个代码都是简单的C++程序,它们都会打印出"Hello World!"。
对于一些已经熟悉C语言的开发者来说,C++的开发会更加方便,因为C++可以无缝地与C语言代码混合使用,同时也可以利用C++的面向对象编程优势。因此,我们在这里不在详细介绍C++的基础语法,而是直接进入C++的其他特性。
namespace命名空间
命名空间的定义
在使用C语言开发时,我们通常会遇到一些问题,如变量名冲突、函数名冲突等。
#include<stdio.h>#include<stdlib.h>intrand=10;intmain(){// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”printf("%d\n",rand);return0;}命名冲突案例1:上面的代码中,我们定义了一个变量rand,但是rand函数在
stdlib.h中已经存在,同时,他们都存在于全局域中,导致编译报错。
#include<stdio.h>intx=10;intmain(){intx=20;printf("%d\n",x);return0;}命名冲突案例2:上面的代码中,
main函数的局部域中存在一个变量x,而全局域中也存在一个变量x,局部域的x会隐藏全局域的x,最终打印出20。
案例1或许在日常开发中,可以通过修改变量名来解决大型项目,不同大型项目,不同模块之间的变量名冲突会更加严重,导致代码维护困难,特别是在有多个团队共同开发的情况下,每个团队可能会定义相同的变量名,导致代码冲突。
而案例2可能会导致代码的不可预测性,因为局部域的x会隐藏全局域的x,导致在main函数中打印出20,而不是10。
为了解决这些问题,C++引入了命名空间的概念。
命名空间是一个特殊的区域,用于存储变量、函数、类等。每个命名空间都有一个唯一的名称,用于区分不同的命名空间。
命名空间的语法如下:
namespacenamespace_name{// 命名空间中的代码}命名空间的使用
命名空间的代码可以包含变量、函数、结构体、以及今后会学习到的类等。
namespaceexm-zem{// 变量intrand=10;// 函数intAdd(intleft,intright){returnleft+right;}// 结构体structNode{structNode*next;intdata;};}命名空间的代码可以嵌套在其他命名空间中。
namespaceexm-zem{intrand=10;intAdd(intleft,intright){returnleft+right;}structNode{structNode*next;intdata;};// namespace嵌套定义namespaceC{intrand=1;}namespaceCPP{intrand=2;}}使用命名空间中的变量、函数、结构体等时,需要使用**双冒号(::)**来访问。在嵌套命名空间中,需要使用多个双冒号(::)来访问。
#include<stdio.h>#include<stdlib.h>namespaceexm-zem{intrand=10;intAdd(intleft,intright){returnleft+right;}structNode{structNode*next;intdata;};// namespace嵌套定义namespaceC{intrand=1;}namespaceCPP{intrand=2;}}intmain(){printf("%d\n",exm-zem::rand);printf("%d\n",exm-zem::C::rand);printf("%d\n",exm-zem::CPP::rand);printf("%d\n",exm-zem::Add(1,2));return0;}输出:
10 1 2 3
上面的代码中,我们定义了一个命名空间exm-zem,在其中定义了一个变量rand,一个函数Add,一个结构体Node,以及两个嵌套的命名空间C和CPP。而在main函数中,我们使用双冒号(::)来访问exm-zem命名空间中的变量、函数、结构体等。
这样,我们在使用rand变量时,既不会和stdlib.h中的rand函数冲突,exm-zem和其中嵌套的C、CPP命名空间中的rand变量之间也不会冲突。
命名空间的作用域
C++中有4中域:
- 全局域
- 局部域
- 类域
- 命名空间域
双冒号就是用于指定所使用的域。当未指定域时(如未使用双冒号(::)时),与C语言相同,默认从局部域开始查找,如果未找到,再从全局域开始查找,不会进入命名空间域进行查找。当双冒号左侧置空时,表示从全局域开始查找。
#include<stdio.h>intx=10;intmain(){intx=20;printf("%d\n",x);printf("%d\n",::x);return0;}输出:
20 10
命名空间的生命周期
在C语言中,对于一个变量、函数、结构体等,它的生命周期取决于它的作用域,在C++中也是如此。
对于命名空间来说,它只能在全局域中定义,因此它的生命周期与全局域相同。我们可以认为,命名空间是一个全局域的单独的子域,用于存储变量、函数、结构体等。
多文件命名空间
在多文件模块化编程中,每个文件都可以定义自己的命名空间,互不干扰。而同名的命名空间会被认为是同一个命名空间,不会冲突。
// file1.cpp#include<stdio.h>namespaceexm-zem{intrand1=10;}// file2.cpp#include<stdio.h>namespaceexm-zem{intrand2=20;}命名空间的展开
在C++中,当我们进行日常小练习或者进行算法竞赛时,很少会出现命名空间冲突的情况,因此我们会直接展开命名空间中的代码,而不是使用双冒号(::)来访问。
#include<iostream>// 标准输入输出流,今后会学习到usingnamespacestd;intmain(){cout<<"Hello World!"<<endl;// cout是标准输出流,endl是换行符,用于在输出后换行,<<是插入运算符,用于将数据插入到输入流中return0;}在这段代码中,我们引入了很多新的概念,如标准输入输出流、endl、<<等。这些概念在今后的学习中会继续介绍。
std是标准命名空间,用于存储标准库中的函数、结构体等。所有C++提供的的标准库函数、结构体等,都被定义在std中,避免了命名空间冲突。
当然,如果你并不想把命名空间中的所有内容都展开,你可以只展开你需要的内容。
#include<iostream>usingstd::cout;intmain(){cout<<"Hello World!"<<std::endl;return0;}只展开cout而不展开endl
初探IO流
在C语言中,我们使用printf和scanf来实现输入输出。在C++中,我们使用cout和cin来实现输入输出。
#include<iostream>usingstd::cout;usingstd::cin;usingstd::endl;intmain(){intx;cin>>x;cout<<x<<endl;return0;}输入:
10输出:
10
相较于C语言中的printf和scanf,cout和cin的使用更加方便,也更加安全。cout和cin是标准输入输出流,而printf和scanf是格式化输入输出函数,它们需要手动处理格式化问题,而cout和cin则不需要手动处理格式化问题。
cout和cin可以自动识别输入输出的数据类型,这也为之后我们将要学习到的类类型提供了更方便的输入输出操作。引入cout和cin的本质目的,就是为了支持任意类型的输入输出操作。
#include<iostream>#include<string>usingnamespacestd;intmain(){string s;cin>>s;cout<<s<<endl;return0;}输入:
Hello World!输出:
Hello World!
这里我们用到了string类,用于存储字符串,之后我们也会详细介绍。cin和cout可以识别并输出字符串,而printf和scanf则不能直接进行这样的操作。
缺省参数
在C++中,我们可以为函数的参数设置默认值,当调用函数时,如果没有提供该参数的值,就会使用默认值,这也被称为缺省参数。
#include<iostream>usingstd::cout;usingstd::cin;usingstd::endl;voidFunc(intx=10){cout<<x<<endl;}intmain(){Func();Func(20);return0;}输出:
10 20
使用缺省参数时需要注意:
- 缺省参数必须是常量表达式,不能是变量。
- 缺省参数的顺序必须与函数的参数顺序一致。
- 缺省参数可以是函数的任意参数,包括非缺省参数。
- 设定缺省参数时,必须从右向左连续设定,不能从左向右设定。
- 传入参数时,必须从左向右连续传入,不能从右向左传入。
对于这上面的1-3条还是比较容易理解的,而4-5条我们来详细解析一下:
如果我们不遵守第四条:
#include<iostream>usingnamespacestd;voidFunc(intx=10,inty){cout<<x<<endl;cout<<y<<endl;}intmain(){Func(30);return0;}那么传入的30究竟是传给x还是y?显然这会引起歧义,因此我们规定:设定缺省参数时,必须从右向左连续设定,不能从左向右设定。
同样,假设我们不遵守第五条:
#include<iostream>usingnamespacestd;voidFunc(intx=10,inty=20,intz=30){cout<<x<<endl;cout<<y<<endl;}intmain(){Func(,30,400);Func(30,,400);return0;}这同样可能会引起歧义,30和400究竟是传给x、y还是z?因此我们规定:传入参数时,必须从左向右连续传入,不能从右向左传入。
正确设定和使用缺省参数的方式:
#include<iostream>usingnamespacestd;voidFunc(intx=10,inty=20,intz=30){cout<<x<<endl;cout<<y<<endl;cout<<z<<endl;}intmain(){Func1();Func1(1);Func1(1,2);Func1(1,2,3);return0;}在进行C语言使用栈时,我们可能会遇到这两种情况:
- 已经确定了栈的大小。
- 不确定栈的大小。
// Stack.h#include<assert.h>#include<stdlib.h>typedefintSTDataType;typedefstructStack{STDataType*a;inttop;intcapacity;}ST;voidSTInit(ST*ps,intn);voidSTPush(ST*ps,intx);// Stack.cpp#include"Stack.h"voidSTInit(ST*ps,intn){assert(ps&&n>0);ps->a=(STDataType*)malloc(4*sizeof(STDataType));// ... 扩容ps->top=0;ps->capacity=n;}voidSTPush(ST*ps,intx){//.. 此处省去具体代码}// main.cpp#include"Stack.h"#include<iostream>#include"Stack.h"usingnamespacestd;intmain(){// 已经确定了有100项ST st1;STInit(&st1);for(size_t i=0;i<100;i++){STPush(&st1,i);}// 不确定有多少项ST st2;STInit(&st2);STPush(&st2,1);STPush(&st2,2);STPush(&st2,3);return0;}当我们无法确定具体大小时,此时只能依靠扩容来满足需求。
而当我们已经可以确定具体大小时,在C语言当中还是只能默认初始化4个或指定的类型的大小,然后反复扩容,导致资源重复消耗。
C++引入了缺省参数,我们可以为函数的参数设置默认值,当调用函数时,如果没有提供该参数的值,就会使用默认值,这也被称为缺省参数。这样,我们既可以满足不确定有多少项的默认初始化4个类型大小,也可以满足这里的已经有确定大小而直接进行指定大小初始化的情况。
另外,在声明与定义函数分离时,缺省参数必须要在声明时指定,不能在定义时指定。需要缺省参数时,通过声明可以直接将缺省参数的值传递给函数,而不需要在调用时传递。
引入缺省参数后修改的程序
// Stack.h#include<assert.h>#include<stdlib.h>typedefintSTDataType;typedefstructStack{STDataType*a;inttop;intcapacity;}ST;voidSTInit(ST*ps,intn=4);// 可以使用缺省参数满足两种情况voidSTPush(ST*ps,intx);// Stack.cpp#include"Stack.h"voidSTInit(ST*ps,intn){assert(ps&&n>0);ps->a=(STDataType*)malloc(n*sizeof(STDataType));// 这里可以根据n来初始化栈的大小// ...ps->top=0;ps->capacity=n;}voidSTPush(ST*ps,intx){//..}// main.cpp#include<iostream>#include"Stack.h"usingnamespacestd;intmain(){// 已经确定了有100项ST st1;STInit(&st1,100);// 已经确定了栈的大小,在这里可以传入具体大小for(size_t i=0;i<100;i++){STPush(&st1,i);}// 不确定有多少项ST st2;STInit(&st2);// 不确定栈的大小,在这里可以使用缺省参数4STPush(&st2,1);STPush(&st2,2);STPush(&st2,3);return0;}本章完