本文主要介绍一些C++语言的基础语法,实际上C++是在C的基础之上,容纳了面向对象的编程思想,且针对C语言的一些不合理的地方进行了优化。下面主要介绍一些C++在C语言之上进行优化的小语法。
1.1命名空间
C++中命名空间语法主要为了解决C语言中无法解决的重名问题,比如下面这段代码,一旦包含了<stdlib.h>的头文件之后,就会报重名错误,因为这个头文件中的rand函数与我们定义的整型变量rand重名,但如果不包含这个头文件,代码却能正常运行。
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
运行结果
因为头文件会在这个测试文件中进行展开,导致编译器无法判断所使用的rand到底是哪个rand,就会造成命名错误。C++中引入命名空间的语法,就是为了解决这样的冲突。
1.1.1命名空间的定义
C++引入了namespace这样的关键字,用来定义命名空间。其语法格式如下
namespace 名字
{
定义变量;
}
下面来利用命名空间解决上面的命名冲突问题。将想要定义的rand变量放入命名空间中,这里的命名空间可以单独理解为一个作用域,将里面的内容全部封装起来了。所以编译器在编译时,不会对其进行搜索。如果想要对其进行访问,需要使用一个操作符,即“::”,下面的代码解决了这一问题
#include <stdio.h>
#include <stdlib.h>
namespace nig
{
int rand = 10;
int Add(int x, int y)
{
return x + y;
}
struct Node
{
int data;
struct Node* next;
};
}
int main()
{
printf("%\n", nig::rand);
return 0;
}
运行结果
可以看到,使用命名空间完美解决了上面命名冲突的问题。在nig这一空间中,还定义了一个函数和结构体,这里主要为了演示“::”的使用,想要访问其中的函数和结构体,语法如下
struct nig::Node newnode;
int z = nig::Add(10, 20);
1.1.2命名空间的其它特性
上面介绍了命名空间的基本使用,这一小节来介绍命名空间的其它使用特性。
- 命名空间可以嵌套
命名空间之间是可以相互嵌套的,也就是说在命名空间中可以定义另一个命名空间,下面的代码演示了命名空间嵌套之后如何对其使用的语法
#include <stdio.h>
namespace nig
{
namespace ver_int
{
int rand = 10;
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
}
namespace ver_float
{
int rand = 10;
void Swap(float* p1, float* p2)
{
float temp = *p1;
*p1 = *p2;
*p2 = temp;
}
}
}
int main()
{
int a = 10, b = 20;
float c = 5.5f, d = 6.5f;
nig::ver_int::Swap(&a, &b);
printf("a=%d,b=%d\n", a, b);
nig::ver_float::Swap(&c, &d);
printf("c=%.2f,d=%.2f\n", c, d);
return 0;
}
运行结果
可以观察到,上面的代码试图去优化由于变量类型不同,而不得不重新定义交换函数这样的冲突,这样可以做到函数名不会因此而改变。但其实C++中有更好的方式解决上面的问题,下文中有提到。
上面的代码虽然使用到了命名空间的嵌套,但它的正确使用场景其实不在这里,实际上,它是为了下面的特性而服务的。
- 命名空间的多文件合并
如果在多个文件中定义了同名的namespace,那么它们会自动合并到一起,就像同一个命名空间一样。在进行一个庞大的项目工程时,每个程序员分工完成自己的那份工作,但难免会有多个程序员使用了相同的命名而导致冲突的问题。所以,命名空间的嵌套,是为了让每个程序员都有自己的命名空间,在独立的命名空间中声明变量及函数,就防止了命名冲突的问题。而在合并时,只需要将它们的命名空间外部再嵌套一层统一的命名空间,这样在编译时就会自动进行合并,进而解决了合并代码而可能造成的命名冲突问题。
1.1.3命名空间的使用
使用命名空间,是有很多种方式的,上文介绍的通过操作符“::”去进行访问,只是其中的一种,本节来介绍另外一种,即使用using将命名空间展开。
- using将命名空间中的某个变量展开
如果一个命名空间中,有一个变量经常被使用,而其它变量很少用到,在这种情况下,使用“::”操作符对经常使用的变量进行访问,会比较麻烦。这时,就可以使用using将该变量进行展开
#include <stdio.h>
namespace nig
{
int a = 1, b = 0;
}
using nig::a;
int main()
{
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", nig::b);
return 0;
}
运行结果
- using展开命名空间中的全部成员
同样地,using能展开某个成员,那么它也能同时展开全部成员,使用方式如下
#include <stdio.h>
namespace nig
{
int a = 1, b = 0;
}
using namespace nig;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
运行结果
在大型项目中,本人并不推荐展开命名空间的全部成员,很容易造成冲突,建议使用“::”操作符去进行访问。而在平时写小程序练习时,展开命名空间的全部成员会非常的便利。
1.2标准输入输出
在C++中有着它自己的一套输入输出的语法规则,它相比于C语言,有了很多优化的地方,但也有一些缺陷,本小节来介绍C++的标准输入输出。在C++中,想要使用标准输入输出,需要包含头文件<iostream>,它是是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
• std::cin是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。
• std::cout是 ostream 类的对象,它主要面向窄字符的标准输出流。
• std::endl是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
• <<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
• std是标准库的命名空间标识符。C++标准库中的函数或对象都是在命名空间std中定义的,因此,当我们需要使用标准库中的函数或对象时,需要用std来限定。
一般在日常练习中,可以使用using将标准库中的成员全部展开,而在做大型项目时并不推荐,下面的代码演示了如何使用C++的标准输入输出
#include <iostream>
using namespace std;
int main()
{
int a = 0, b = 0;
cout << "请输入两个整数:";
cin >> a >> b;
cout << "a=" << a << ",b=" << b << endl;
return 0;
}
运行结果
另外,在使用cin和cout输入输出时,它们可以自动识别变量,不需要像scanf函数和printf函数那样用占位符去指定变量类型。
1.3函数的相关优化
C++对于C语言中的函数的某些缺陷,进行了相应的语法优化,如缺省参数和函数重载,它们分别解决了不同的缺陷。
1.3.1缺省参数
缺省参数是声明或定义函数时为函数参数指定的一个缺省值。在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参(有些地方把缺省参数也叫默认参数)。在下面的场景中,使用fun函数来动态开辟一个数组,进行数据存储,但如果一开始并不知道数组的大小,那么就可以给该函数一个缺省参数作为默认值(下面的代码中为10)。如果想要指定大小,就直接传入参数即可
#include <stdio.h>
#include <stdlib.h>
int* fun(int n = 10)
{
int* p = (int*)malloc(sizeof(int) * n);
if (p == NULL)
{
perror("malloc");
exit(1);
}
return p;
}
int main()
{
int* arr1 = fun();
for (int i = 0; i < 10; i++)
{
*(arr1 + i) = i + 1;
cout << *(arr1 + i) << " ";
}
int* arr2 = fun(5);
cout << endl;
for (int i = 0; i < 5; i++)
{
*(arr2 + i) = i + 1;
cout << *(arr2 + i) << " ";
}
return 0;
}
运行结果
缺省参数分为全缺省和半缺省参数,全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。上面案例中的缺省参数即为全缺省参数。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值,且调用带缺省参数的函数时,C++规定必须从左到右依次给实参,不能跳跃给实参。
#include <iostream>
using namespace std;
//全缺省参数
void fun1(int a = 10, int b = 20, int c = 30)
{
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
//半缺省参数
void fun2(int a, int b = 10, int c = 20)
{
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
int main()
{
fun1();
fun1(1);
fun1(1, 2);
fun1(1, 2, 3);
fun2(100);
fun2(100, 200);
fun2(100, 200, 300);
return 0;
}
运行结果
另外,函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值,这里就不进行演示了。
1.3.2函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。在下面的情况之下,可以实现函数重载。
- 参数类型不同
函数重名,但参数类型不同,可以实现函数重载。这样做的好处是,同一个函数名可以支持多种类型的函数参数,比如下面的代码
#include <iostream>
using namespace std;
int Add(int x, int y)
{
cout << "int Add(int x, int y)" << endl;
return x + y;
}
double Add(double x, double y)
{
cout << "double Add(double x, double y)" << endl;
return x + y;
}
int main()
{
int a = 10, b = 20;
float c = 12.5f, d = 23.5f;
printf("%d\n%.2f\n", Add(a, b), Add(c, d));
return 0;
}
运行结果
上面的代码,通过函数重载,实现了同一函数名称实现不同类型函数参数的功能实现。所以,还记得上文提到的交换函数吗?当时使用的是命名空间去实现这样的功能。但其实,在C++中,使用函数重载是可以更加润滑的实现上述功能。这个就留给读者去自行尝试了。
- 参数个数不同
参数的个数不同,也可以实现函数重载
#include <iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(1);
return 0;
}
运行结果
- 参数类型顺序不同
参数类型的传递顺序不同,也可以实现函数重载
#include <iostream>
using namespace std;
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
f(10, 'a');
f('a', 10);
return 0;
}
运行结果
函数重载虽然提供了很多便利,但隐约的也埋下了一些隐患。其中,函数有没有返回值是无法作为函数重载的条件的。因为调用时无法进行区分
void fxx()
{}
int fxx()
{
return 0;
}
另外,函数重载在和缺省参数结合使用时,也会出现歧义。下面的两个函数构成重载f()但是调用时,会报错,存在歧义,编译器不知道调用哪个函数。
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
1.4引用
C语言中,虽然指针的功能非常强大,给予了程序员很大的权限,但它比较复杂,使用起来也容易出现一些问题。C++提供了引用这一语法,来补充C语言中指针的一些缺陷。
1.4.1引用的概念和定义
引用的语法格式如下
类型& 引用别名 = 引用对象;
需要说明的是,引用不是新定义的一个变量,在语法层面上,引用仅仅是给引用对象起了一个别名,并没有开辟新的空间,它和它引用的对象使用的是同一块空间。下面的代码中,定义了变量a,其中b,c,d都是变量a的别名
#include <iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int& c = b;
int& d = c;
d++;
printf("%d,%d,%d,%d\n", a, b, c, d);
return 0;
}
运行结果
1.4.2引用的特性与使用
下面补充一些引用的特性,之后来看看引用在一些实际场景中的使用。引用在定义时必须初始化,不进行初始化编译会无法通过。一个变量可以有多个引用,上一小节的代码中,变量a就有多个引用。引用一旦引用了一个实体,就不能再次引用其它的实体。下面的代码中,c=b为赋值语句,将b的值赋给a的别名,也就是c,它并没有改变引用对象
#include <iostream>
using namespace std;
int main()
{
int a = 10, b = 20;
int& c = a;
c = b;
cout << c << endl;
return 0;
}
引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。引用做返回值的场景在后面类和对象的文章中会详细介绍,下面先介绍引用传参这一应用。
下面的代码中,演示了利用引用实现两数交换的函数。由于引用是对引用对象的别名,所以参数列表中的变量a,b和传入的参数管理的是同一块空间,并没有作为形参独立开辟空间。比C语言中使用指针变量去实现写起来方便很多
#include <iostream>
using namespace std;
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
printf("a=%d,b=%d\n", a, b);
return 0;
}
运行结果
1.4.3const引用
C++中允许应用const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。下面的代码中,编译后会进行报错,这里的引用对a的权限进行了放大
const int a = 10;
int& b = a;
正确的写法应该像下面这样,但由于const的修饰,变量还是无法进行修改
const int a = 10;
const int& b = a;
//b++; //报错,error C3892: “b”: 不能给常量赋值
但下面的代码是可以编译通过的,引用对变量c的权限进行了缩小,所以进行引用之后无法再对其进行修改
int c = 20;
const int& d = c;
//d++; //报错,error C3892: “d”: 不能给常量赋值
C++中在某些场景下,需要对临时对象进行引用。C++中临时对象具有常性,所以这里触发了权限放大,必须要常引用才可以。所谓临时对象,就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
下面的代码中,a*3就是一个临时对象,想要对其引用必须用const修饰
int a = 10;
//int& b = a * 3; //编译错误,触发了权限放大
const int& b = a * 3;
下面的代码中,由于进行了隐式的变量类型转换,而在进行转换时,数据作为临时对象存放在一块空间中,想要对其引用也要用const修饰
double c = 12.34f;
//int& d = c; //编译错误,触发了权限放大
const int& d = c;
double& d = c; //不进行类型转换,该变量就不作为临时变量,可以运行
1.4.4指针和引用的联系
在C++中,指针和引用在实践中是相辅相成的,谁也无法完全的代替谁,虽然功能有重叠性,但也各有特点。下面总结一些特性
• 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
• 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
• sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
1.5内联函数与空指针
内联函数与C++中的nullptr即空指针,也是为了解决C语言中的一些语法缺陷
1.5.1内联函数
C语言在实现宏及宏函数时,它的底层原理与普通的函数不太一样,它并没有建立新的函数栈帧,而是将代码中用到该宏的地方进行直接替换。这里在使用时其实产生了一些隐患。请看下面的代码
#define ADD(int a, int b) return a + b;
#define ADD(a, b) a + b;
#define ADD(a, b) (a + b)
哪个宏函数是正确的呢?如果C语言功底较为扎实的读者,不难看出,第三种写法才是正确的,前两种或多或少会有些问题。由于宏的底层实现是直接进行替换,所以第一种将return语句替换进去后,程序就会直接结束。而第二种会多替换进去一个分号,也会有些影响。为了解决这种隐患,C++引出了内联函数的概念。
在函数前加上inline关键字进行修饰的函数就是内联函数,编译时,它在被调用时不会建立新的函数栈帧,而是直接进行展开,进而提高效率
#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%d\n", Add(10, 20));
return 0;
}
C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。需要补充说明的是,inline对于编译器而言仅仅是一个“建议”。也就是说,即使加上inline,该函数如果展开后特别复杂会影响效率,那么编译器在运行时就不会展开。至于这个所谓的“复杂”其实没有标准,完全取决于编译器。
1.5.2空指针nullptr
C语言中的NULL关键字在头文件中是这样定义的
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
实际上它是一个宏,但这其实埋下了一些隐患。在C++中,当它和函数重载一起使用时,会出现一些问题,如下面的代码
#include <iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
return 0;
}
这样运行时,由于NULL的本质是一个宏,被定义成了0,在上面变量类型不同的函数重载代码中使用时,会出现歧义。调用f(0)时,编译器并不知道调用哪一个函数,会与程序原本的意愿相违背。
C++11中引入nullptr,nullptr是⼀个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。这样就可以解决上面的问题了。
总结一下,本节介绍的一些C++中的语法,主要是为了弥补C语言上的一些缺陷。在本文开篇就提到过,C++的目的其实是为了在C语言的基础上引入面向对象的程序设计思想,在这种情况下,不得不对C语言中一些有缺陷的语法进行革新。