C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
在前面对C语言的学习中,相信大家对用C语言编程已经很了解了,所以就不赘述基础的语法了。在本篇文章中将介绍一些新知识,可以让你初步上手C++:
首先要了解的就是C++的输入与输出:
cout是标准输出对象,用于向标准输出流(屏幕)写入数据;
endl是一个操纵符,写入endl的效果为换行,并将缓冲区中的数据刷新到设备中;
使用输出运算符 << 可以在标准输出上打印信息:
std::cout << 表达式1 << 表达式2 << sts::endl;
cin是标准输入对象,用于从标准输入流(键盘)读取数据;
使用输入运算符>> ,从给定的输入流中读取数据放到给定的对象中:
std::cin >> 变量1 >> 变量2;
使用cin与cout输入输出时,可以自动识别输入或输出的类型,而且不需要取地址的操作。相较于C语言的需要写出输入输出的格式。
使用cin、cout、endl时需要包含头文件
,并且由于它们的实现都在C++标准库命名空间std
中,所以使用时需要引入命名空间或域作用限定符使用。
接下来就介绍关于命名空间的知识:
在C语言中,经常会遇到自己定义的变量或函数与库中定义的函数命名冲突的情况,或是在项目中自己定义的函数与别人定义的函数命名冲突的情况,这样的情况往往令人厌烦。
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染
定义命名空间时,要用到关键字namespace,后面加命名空间的名称,然后在后面的{}中定义成员(变量或函数等):
namespace qqq
{
int rand = 0;
int max(int a = 0, int b = 0)
{
if (a > b)
{
return a;
}
return b;
}
}
在上面的命名空间qqq中定义的rand变量与max函数,这两个符号都是在库中实现的函数,在命名空间qqq中定义这两个字符,就相当于将这个rand与max放在命名空间域中,而不是暴露在全局域,就不会发生命名冲突。
命名空间可以嵌套,也可以定义多个名称相同的命名空间(编译时会和为一个)。
使用命名空间中的成员变量或成员函数时,有三种方式:
使用域作用限定符::使用某个成员时,每次使用某个在命名空间中的成员时,都要显式的写出命名空间名::成员名
:
//使用域作用限定符::使用某个成员
//省略头文件包含与命名空间qqq
int main()
{
int a = qqq::rand;
int b = 10;
std::cout << qqq::max(a, b);
return 0;
}
使用using namespace 命名空间;
名引入整个命名空间域时,就相当于将整个命名空间域全部暴露在全局域中,命名空间中的成员就成了全局的。使用时就可以直接使用:
//使用using namespace 引入整个命名空间域
//省略头文件包含与命名空间qqq
using namespace qqq;
using namespace std;
int main()
{
int a = rand;
int b = 10;
cout << max(a, b);
return 0;
}
上面的方式虽然方便,但是将命名空间的成员暴露在全局,就使命名空间丧失了其原有的意义。但是对于某些使用频繁的函数,显式的说明又太繁琐,所以我们可以使用using 命名空间名 :: 成员名;
的方式来引入某个成员,相当于将这个成员暴露在全局,后面在使用这个成员时可以直接使用,但别的成员依旧需要显式说明:
//使用using引入某个成员
//省略头文件包含与命名空间qqq
using qqq::max;
using std::cout;
int main()
{
int a = qqq::rand;
int b = 10;
cout << max(a, b);
return 0;
}
对于上面提到的cin、cout、endl,都在标准库命名空间std中实现,在使用时当然可以直接展开标准库:using namespace std;
,也可以每次使用时都std::
,但是最安全便捷的方法就是引入常用的成员:using std::cout;
(例如上)。
除此之外,所有的库函数都在std中。
在需要对对象初始化时,相对于C语言中的#define定义一个常量,用缺省参数设置一个默认值的方式要方便许多。
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include
void add(int a = 1, int b = 2)
{
std::cout << a + b << std::endl;
}
int main()
{
add();
add(10);
return 0;
}
需要注意的是:为了避免定义与声明的默认值不同,函数的定义与声明不能同时设置默认值。
根据参数列表是否完全设置默认值,分为全缺省与半缺省:
全缺省参数就是参数列表全部设置默认值。
在调用全缺省参数的函数时,如果传参,实参会从左到右依次给参数列表中的形参赋值,不能跳着赋值:
#include
void Func(int a = 1, int b = 2, int c = 3)
{
std::cout << a << " " << b << " " << c << std::endl;
}
int main()
{
//Func(10, , 30);错误调用
Func();//正确调用
Func(10, 20);
return 0;
}
半缺省参数就是参数列表不全部设置默认值。
半缺省参数只能从右向左依次给出默认值;调用时的实参不能少于未给默认值的参数个数,也是从左向右依次赋值给实参:
#include
//void Func(int a = 1, int b, int c = 3){}错误定义
void Func(int a, int b = 2, int c = 3)
{
std::cout << a << " " << b << " " << c;
}
int main()
{
//Func();错误调用
Func(10);//正确调用
return 0;
}
在实现函数时,经常会有一些函数的作用类似,只是参数的类型有所不同。如果定义几个函数名不同但功能类似的函数,在调用时就会不方便。就有了函数重载:
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
在定义函数重载时,函数名相同,参数个数不同、参数类型不同、参数类型顺序不同都可以构成重载:
int Func(int a, int b = 20)
{
cout << 1 << endl;
return 1;
}
int Func(double a, int b)//与第一个函数参数类型不同
{
cout << 2 << endl;
return 2;
}
int Func(int a, double b)//与第二个函数参数顺序不同
{
cout << 3 << endl;
return 3;
}
int Func(int a)//与第一个函数参数个数不同
{
cout << 4 << endl;
return 4;
}
需要注意的是:当参数列表相同时,返回值的不同不构成重载:
//错误代码,会报错:无法重载仅按返回类型重载的函数
int Func(int a, int b = 20)
{
cout << 1;
return 1;
}
void Func(int a, int b)
{
cout << 1;
}
调用重载的函数时,由于函数有缺省参数,比如当调用时只传一个参数时,就不知道要调用第一个参数没有默认值的函数,还是只有一个形参的重载函数,还是其他可以只传一个参数的函数了。所以需要特别注意避免出现二义性:
Func(10);//错误调用。对于传一个整型,第1个函数与第4个函数均可
Func(10, 30);
Func(1.1, 5);
Func(5, 1.1);
那么为什么C++支持函数重载?
在C语言进阶部分介绍过,我们编写的代码在转化为可执行程序之前,要经历:预编译、编译、汇编、链接的过程。
在编译时,会生成符号表,符号表包括符号名与地址。根据函数名修饰规则,每一个不同的函数都会有自己独特的符号,当出现两个符号相同的函数时,就会报编译错误(即符号重定义)。
只是在C语言编译器修饰后,符号名就是函数名同名的函数就会重定义;而C++的的编译器修饰后的符号名中会包含函数名、参数列表的信息。重载的函数符号名是不同的,不会重定义,调用时虽然函数名相同,但其实调用的是不同的函数。
但是由于修饰规则中没有包含返回值类型,所以函数名与参数列表相同但返回值不同的函数符号名也是相同的,所以不能构成重载。
引用就是起别名,因为它与其引用的变量本质上同用一块空间,所以可以通过引用变量改变其引用的变量的值,而且在语法上不开辟新的空间。
在类型后加 & 就可以定义一个引用变量:类型& 引用变量名 = 引用实体;
int main()
{
int a = 10;
int& ra = a;
int& rra = ra;
cout << a << " " << ra << " " << rra << endl;
rra = 20;
cout << a << " " << ra << " " << rra << endl;
return 0;
}
在上面的代码中,定义了整型变量a,引用变量ra是a的别名,rra是ra的别名,即rra也是a的别名。它们三个名称共用一块空间,当改变其中一个名称的值时,另外两个的值也会发生改变。
在定义引用变量时,一定要注意一些问题:
int& a;//错误代码,必须初始化引用
int a = 10;
int b = 5;
int& ra = a;
int& rra = ra;
//int& ra = b; 错误代码,ra重定义
我们定义一个引用变量,就可以通过这个引用来改变实体的值。但是对于具有常属性的实体变量,给它起一个可以改变其内容的别名显然是非常危险的。
所以对于有常属性的变量,只能给常引用初始化(即引用权限不能放大):
const int a = 10;
//int& ra = a; 错误代码,权限放大
const int& ra = a; //正确代码
当然,如果给一个非常量定义常引用,即权限的缩小或平移,是可以的:
int b = 5;
const int& rb = b;//正确代码,权限缩小
int& rrb = b;//正确代码,权限平移
不仅是const修饰的变量,传值的返回值、发生类型转换的变量都具有常属性。因为它们在转换的时候会隐式拷贝一份到临时变量,再由临时变量赋值,而这个临时变量是有常属性的。
int c = 2;
//double& rc = c; 错误代码,引用与实体的类型不同
在之前,对于一些需要返回型参数的函数实现,我们只能使用指针传参,这样不论在传参还是调用时都很麻烦。而用引用作为返回型参数就会很方便,比如这个交换函数:
//命名空间展开与头文件包含已省略
void Swap(int& a, int& b)
{
int temp = b;
b = a;
a = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
cout << a << " " << b;
return 0;
}
传参时不用取地址,交换时不用解引用,看起来也很清楚,更适合做返回型参数。
在之前的返回值只能传值返回,或是传指针返回。传值返回时,是有隐式的拷贝过程的,对于大对象而言非常影响效率;传指针返回写起来也不方便。传引用返回就既效率高又直观:
这样的代码,看起来没有问题,也实现了Add的作用,但是却有一个很大的问题。就是传引用返回了一个局部变量。这种行为是很危险的,因为局部变量在生命周期结束后,空间就会还给操作系统,返回的这个引用就是一个不确定数据的别名(如果环境会清除数据,就会返回任意值)。
所以在使用引用返回时需要特别注意,引用返回的值一定不能出了作用域就被销毁,可以返回静态变量等:
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
cout << c;
return 0;
}
需要注意的是:引用虽然在语法上没有开辟新的空间,但在底层逻辑上与指针是一致的,也需要开辟空间存放地址。
在C语言部分,对于简单且调用频繁的函数,我们可以使用宏函数的方式,在预编译阶段就宏替换,而节约函数开辟栈帧的时间与空间以提高效率。比如宏函数ADD:
#define ADD(a, b) ((a)+(b))
由于宏函数是直接替换,而不是传参,想要宏函数的逻辑没有问题,写法显然是有些繁琐的。
内联函数就可以解决这样的问题:
以inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率:
inline int Add(int a, int b)
{
return a + b;
}
需要注意的是:
到此,关于C++入门的一些知识就介绍完了,以后就会持续更新C++的知识,欢迎大家持续关注
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
C++之路才刚刚开始,我们一起加油吧!