好久不见,本篇介绍一些C++的基础,没有特别的主题,话不多说,直接开始。
1.C++的第一个程序
C++中需要把定义文件代码后缀改为 .cpp
我们在 test.cpp 中来看下面程序
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
相信大家再熟悉不过了,这是一个C语言的简单代码,我们运行代码发现也没问题
这是因为C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行,当然,C++也有自己的一套输入输出,严格来说C++版本的hello world应该像下面这样写
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
啊?namespace std是什么啊?cout啥意思啊?<< 这又是啥?......
相信大家初次看到C++的代码一定会有这些疑惑,不着急,我们一个一个来简单看看。
2.命名空间namespace
2.1 namespace的价值
namespace是我们接触的C++的第一个关键字。
我们先看下面的程序,从程序现象分析namespace的价值
#include <stdio.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
此时,上面的程序可以正常运行,不会报错
但是我们如果加上#include <stdlib.h>呢?
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
程序就会报下面的错误,说rand重定义了
多加了个头文件程序为什么会重定义呢?
在C语言中有个东西叫命名冲突,在同一域内,不能定义同名的东西。没有包含 <stdlib.h> 时,全局只有一个rand变量,此时没有冲突,程序正常运行,但是,包含 <stdlib.h> 这个头文件后就有问题了,是因为rand是C语言库里的函数,头文件stdlib.h包含这个库函数,所以变量rand和库函数rand两个名字就冲突了
为了解决C语言中命名冲突的问题,C++设计了namespace,命名空间
2.2 namespace的定义
定义命名空间,需要使用namespace关键字,后面跟命名空间的名字,然后接一对{},{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
2.2.1 namespace定义变量
以上面的rand为例,定义一个命名空间,命名空间的名字随便起嗷
namespace lyj
{
int rand = 10;
}
#include <stdio.h>
#include <stdlib.h>
namespace lyj
{
int rand = 10;
}
int main()
{
printf("%d\n", rand);
return 0;
}
namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量
此时再看我们的程序,运行没有报错,变量rand和函数rand已经不在同一个域内了,就不会冲突
虽然程序没有报错,但是报了如下警告
报警告是因为此时这里的rand访问的是全局的函数,并不是命名空间里的变量rand,解除警告可以把%d改成%p,就是打印函数rand的地址。
那我们要访问namespace里的rand怎么访问?
2.2.2 域作用限定符::
我们要知道一个运算符 :: 两个冒号,叫做域作用限定符,先看下面的代码
#include <stdio.h>
int a = 20;
int main()
{
int a = 10;
printf("%d\n", a);
return 0;
}
根据局部优先的规则,我们打印出来的应该是10,而不是20
我们用了::这个运算符后,并且::的左边什么也不给(左边留空格只是为了格式好看),就默认去全局查找,如下
printf("%d\n", ::a);
那么打印出来的就是20
理解了之后我们回过头来看上面的rand,想要在命名空间查找,::左边就是命名空间的名字,如下
#include <stdio.h>
#include <stdlib.h>
namespace lyj
{
int rand = 10;
}
int main()
{
printf("%d\n", lyj::rand);
return 0;
}
此时就能解决名字冲突的问题,想访问哪个域就访问哪个域
C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所以有了隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找的逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量的生命周期
2.2.3 namespace定义函数和类型
这里简单举几个例子,比如下面的命名空间,定义了一个简单的函数和结构体
namespace lyj
{
int add(int a, int b)
{
return a + b;
}
struct node
{
struct node* next;
int val;
};
}
函数直接指定就行了
int main()
{
printf("%d\n", lyj::add(1, 1));
return 0;
}
要定义结构体的话就稍微不同一些 ,命名空间的名字放在struct的后面,结构体名字的前面
int main()
{
struct lyj::node p1;
return 0;
}
2.2.4 namespace的嵌套定义和合并
namespace只能定义在全局,当然它还可以嵌套定义
namespace group
{
namespace zhangsan
{
int num = 1;
int add(int a, int b)
{
return a + b;
}
}
namespace lisi
{
int num = 2;
struct node
{
struct node* next;
int val;
};
}
}
比如上面这个例子,就是namespace的嵌套定义。如果我们要分别打印group下面张三的num和李四的num,像下面这样就行了
printf("%d\n", group::zhangsan::num);
printf("%d\n", group::lisi::num);
如果代码复杂一点,有几个头文件和源文件,那此时怎么运用命名空间呢?
其实,不同文件的命名空间,名字相同会自动合并 。当然,这个合并并不是真的合并在一起,只是逻辑上的合并,名字相同的namespace会认为是一个namespace。举个简单例子
在Func.h文件中有下面的函数声明
namespace func1
{
int Add(int a, int b);
int Sub(int a, int b);
}
namespace func2
{
int Mul(int a, int b);
float Div(int a, int b);
}
在Func.cpp文件中有下面的函数
#include "Func.h"
namespace func1
{
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
}
namespace func2
{
int Mul(int a, int b)
{
return a * b;
}
float Div(int a, int b)
{
return (float)a / b;
}
}
虽然他们不在同一个文件,但是namespace func1中的内容和namespace func2中的内容都会被分别合并
2.3 C++中 std命名空间
2.3.1 关键字using
我们看下面的例子
namespace lyj
{
int a = 1;
int b = 2;
}
想要打印namespace里的a和b,就要像下面这样用::去一个个指定
printf("%d\n", lyj::a);
printf("%d\ ", lyj::b);
如果namespace里我们需要用的东西很多,像这样一个个指定真的太麻烦了,那么直接把整个namespace展开可以吗?可以。展开namespace就要用到关键字using。
using namespace lyj;
加上这句话之后,就可以直接使用namespace里的东西
namespace lyj
{
int a = 1;
int b = 2;
}
using namespace lyj;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
但是,展开命名空间中所有成员,在实战项目中不推荐,冲突风险很大
假设我在程序中用a用的非常多,不推荐展开全部,一次次展开又太麻烦了,怎么办?
using还可以局部展开。比如说我就展开a。
using lyj::a;
使用时,a就不需要用::去指定,可以直接使用,b没展开就要用::指定
2.3.2 std命名空间
C++标准库都放在一个叫std(standard)的命名空间中。
using namespace std;
这句话就是把C++标准库都展开。在日常练习中可以展开,不会有什么风险。
3.C++的输入和输出
<iostream>是 Input Output Stream 的缩写,是标准输入、输出流的库,定义了标准的输入、输出对象。
C++的输入输出需要包含这个头文件,就相当于C语言中使用printf我们需要包含<stdio.h>这个头文件。那为什么iostream没有加.h呢?C++标准库版本问题,新版本是不需要加.h的,如果加了.h,证明那个编译器应该是特别老的编译器(比如vc6.0)。
std::cin 是istream类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流 。
std::cout 是ostream类的对象,它主要面向窄字符的标准输出流。
std::endl 是一个函数(endl是endline的缩写),流插入输出时,相当于插入一个换行字符加刷新缓冲区(目前简单理解为换行符,其实这个函数很复杂)。
这里cin、cout中的c就是窄字符的意思,后面会学宽字符,目前我们遇到的都是窄字符。
当我们在程序中展开了std,也就是using namespace std; 使用cin, cout, endl时就不需要加std::了,不过展开了std又加上了std:: 程序也不会报错,但有点多此一举了。
<<是流插入运算符,>>是流提取运算符。
C语言中用 << 和 >> 作为位运算符,<<是左移运算符,>>是右移运算符。
<<和>>是二元操作符,只能有两个操作数!
到现在,我们可以简单抽象地理解下面这句话了,就是把hello world 和endl 流向cout,而cout就是插入到终端或者控制台,其中endl简单来说就是换行符。
cout << "hello world" << endl;
C++输入、输出可以自动识别变量的类型
C语言中的printf和scanf需要手动指定格式,比如输入一个int a ,输出一个float b = 3.14
int a;
float b = 3.14;
scanf("%d", &a);
printf("%f", b);
而C++中就不需要指定格式,比如下面的cout输出
int a = 1;
float b = 3.14;
char c = 'x';
cout << a << " " << b << " " << c << endl;
再来看看cin输入,也是不需要指定类型的
int y, z;
cin >> y;
cin >> z;
cout << y << endl << z << '\n';
也可以cin >> y >> z;
类型不同也可以。
还有一点,
在C语言中想要确定小数点的精度,像下面这样就行,
double d = 2.22222222;
printf("%.2lf\n", d);
在C++中小数精度是默认小数点后5位,
在C++中可以控制小数点精度吗?可以但是比较复杂。所以我们需要在C++中确定精度的话直接用C语言的printf就行了,反正C++兼容C语言。
由于兼容和缓冲区等复杂原因,C++的cin和cout的IO效率有时候比较低,可以与scanf和printf混着用,在io需求比较高的地方,比如大量输入输出竞赛中,也可以加上下面的三句代码,提高C++IO效率。
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
本小篇就到这里,拜拜~