【C++要笑着学】第一章:关键字 | 命名空间 | 输入和输出

举报
柠檬叶子C 发表于 2022/04/02 20:20:03 2022/04/02
【摘要】 本章将正式开始 C++ 的学习,将深入浅出地讲解。本篇将以HelloWorld 开始,以 HelloWorld 结束。通过这段代码,讲解C++中的命名空间和输入与输出。话不多说,让我们开始吧!


 


写在前面:

本章将正式开始 C++ 的学习,将深入浅出地讲解。本篇将以 HelloWorld 开始,以 HelloWorld 结束。通过这段代码,讲解C++中的命名空间和输入与输出。话不多说,让我们开始吧!



Ⅰ.  初探 HelloWorld

“深”入浅出

 所有的伟大,都源于一个勇敢的开始。学习一门语言当然是要从 HelloWorld 开始!

① 首先打开我们的编译器,我用的是 VS2077 VS2022:

打开编译器创建完新项目后,右键源文件,点击 "添加新建项" ,名称我们就取为 test.cpp,这样我们的代码就能很好地创建出来了:


💬 创建完毕后,我们就可以开始打代码了:

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello,World!" << endl;

	return 0;
}

如果你不知道 using namespace、cout、endl 这些是什么,这都没有关系,我们下面会慢慢讲解,不用着急。

🚩 完成后运行一下代码:

  成功打印出来了。


💬 我们再试一试C语言的 HelloWorld:

#include <stdio.h>

int main()
{
	printf("Hello, World!");

	return 0;
}

 🚩 运行结果如下:

💡 我们发现,也是可以输出结果的。因为 C++ 是 C语言的超集。


  好了,我们已经精通C++ 了,本专栏完结撒花!  

     



Ⅱ.  关键字

0x00 关键字

  C++ 共计 63 个关键字,其中包括 C 语言的 32 个关键字。

这里我们只是看一下 C++ 有多少关键字,不会对关键字进行具体的讲解,我们后续都慢慢会学到的。这里把关键字表列出来,只是为了能够混个眼熟,我们命名时要避开这些关键字!


C++98 关键字



Ⅲ.  命名空间

0x00 问题引入

在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都会存在于全局作用域中,这么一来就会导致命名的冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。 我们刚才 HelloWorld 代码中的 namespace 的出现,就是针对这个问题的。


💬 为了能够让大伙理解命名空间的存在是多么的合理,我们来故意踩一下命名冲突的坑。

 在 stdlib 库中有一个生成随机数的函数 rand() ,相信大家都认识,但是我们假装某个人不知道 stdlib 库中有一个叫 rand 的函数存在,因此在定义变量时给变量取名为 rand

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int rand = 233;
	printf("%d\n", rand);	 // 这里到底是打印我们自己定义的rand,还是stdlib里的?

	return 0;
}

  我们知道,#include 包含头文件,头文件里的内容是会被展开来的。当展开头文件时,stdlib 库中有一个叫 rand 的函数,我这里又定义了一个叫 rand 的变量,此时就冲突了!

 冲突了,那么问题来了,我们这里 printf 打印出来的会是什么呢?

编译器的寻找规则: 局部找 → 全局找 → 找不到(报错)。

或许他打印出什么并不重要,但是大家应该能够体会到命名冲突多是一件倒霉事了。


😂 问题是在C语言里几乎没有办法很好地解决这种问题。

所以为了很好地解决这种冲突的问题,C++ 就加入了命名空间的特性!

在 C++ 里,我们就可以利用 "命名空间" 来解决这个问题,所以 C++ 提出了一个新语法 —— 命名空间 namespace



0x01 命名空间的定义

  定义命名空间,肯定得用到我们刚才提到的 namespace 关键字,namespace 后面可以取一个空间名,然后再接上一对大括号就可以了,上图!

 那我们该如何使用它呢?

💬 我们来拿上面的代码来举例子,试试用 namespace 来解决命名冲突的问题:

namespace nb {  
	int rand = 233;
}

int main(void)
{
	// int rand = 233;
	printf("%d\n", nb::rand);    // 这样我们就能看出rand是nb这块命名空间里的变量了

	return 0;
}

🔑 画图详解:

💡 这里的 : :  叫做 "作用域限定符" 。这么一来,就不怕冲突了,问题就这么轻松地解决了。

命名空间能够达到一种类似于 "隔离" 的效果。


📌 注意事项:

 注意!!!

命名空间必须在全局作用域下定义!

其次,正是因为命名空间是全局的,所以这个 rand 变量也自然而然地变成了全局变量。

② 命名空间长得有点像结构体,但是他和结构体不是一个东西,结构体是定义一个类型,它们的性质是完全不一样的。还有,命名空间大括号外不用加分号,手残党要注意了。


 

0x02 命名空间里的内容

📚 命名空间里的内容,不仅仅可以存放变量,还可以在里面放函数,结构体,甚至是 类(我们后面会讲)。

💬 代码演示:

#include <stdio.h>

namespace N1 {
	int a = 10;
	int b = 20;
	int Add(int x, int y) {
		return x + y;
	}
}

namespace N2 {
	int c = 0;
	struct Node {
		struct Node* next;
		int val;
	};
}

int main(void)
{
	int res = N1::Add(N1::a, N1::b);
	printf("result = %d", res);

	struct N2::Node node1;

	return 0;
}


0x03 命名空间的嵌套

📚 没想到吧,命名空间是可以套娃的,命名空间可以嵌套命名空间。

💬 嵌套方法演示:

// 国家5A级景区...
namespace AAAAA {
	int a5 = 10000;
	namespace AAAA {
		int a4 = 1000;
		namespace AAA {
			int a3 = 100;
			namespace AA {
				int a2 = 10;
				namespace A {
					int a1 = 1;
				}
			}
		}
	}
}

int main(void)
{
	// 取出 a1 
	int ret = AAAAA::AAAA::AAA::AA::A::a1;

	return 0;
}

📌 注意事项:虽然套了这么多层,但是它们仍然都是全局的,只是套在了一个命名空间内而已。


0x04 空间名重名问题

❓ 命名空间是用来解决命名冲突问题的,那我项目中定义的某个命名空间的名字和其他命名空间的名字冲突了怎么办?

 这话可能有点绕,不过没有关系,让我品一品82年的拉菲先。

其实, 在同一个工程中是允许存在多个相同名称的命名空间的。

编译器最后会将他们合成到同一个命名空间中的。有两个相同名字的命名空间,就会合二为一。有三个相同名字的命名空间,就会三合一……


就算是有七颗龙珠,也召唤不出神龙,只会变成一个更大的 "龙珠"

💬 不信?这就上号给你们证明一波!

(证明:编译器会将重名的命名空间融合)

 🔺 总结:同一个工程中允许存在多个相同的命名空间,编译器最后会将它们合成到一起。


0x05 展开的方式

💬 假设有这样的一种情况,一个命名空间中的某个成员是我们经常要使用的:

#include <stdio.h>

namespace N1 {
	int a = 10;  // 假设a经常需要使用
	int b = 20;
	int c = 30;
}

void func(int n) {
	printf("HI, %d\n", n);
}

int main(void)
{
	 printf("%d\n", N1::a);
	 int res = N1::a;
	 func(N1::a);
	 printf("hello, %d\n", N1::a);

	return 0;
}

  指定的作用域,能够做到最好的命名隔离,但是使用起来好像不是很方便。

每次使用都要调 : :  ,好捏🐎烦!这也太难受了,有办法能解决吗?

这位同学请不要激动!命名空间是可以展开的。


💡 我们可以用 using namespace 将整个命名空间展开,因为命名空间是在全局土生土长的,所以展开后,里面的东西自然会被展开到全局。

using namespace 空间名;


💬 我们试一试:

#include <stdio.h>

namespace N1 {
	int a = 10;  // 假设a经常需要使用
	int b = 20;
	int c = 30;
}

void func(int n) {
	printf("HI, %d\n", n);
}

using namespace N1;  // 将N1这个命名空间展开

int main(void)
{
	 printf("%d\n", a); // 这样我们就可以直接使用了,就不需要 "::" 了
	 int res = a;
	 func(a);
	 printf("hello, %d\n", a);

	return 0;
}

 但是!!! 全部展开,虽然使用起来方便,但是隔离的效果失效了! 

❓ 那我们岂不是白写命名空间了?

 所以一定要慎用!我们还是不建议大家把命名空间都展开的。


🔑 解决方法:我既要用起来方便,又要保持隔离的效果!小孩子才做选择!我全都要!

  

📚 指定展开某一个,直接使用 using 将命名空间中某个成员引入。

可以用来展开命名空间中最常用的成员:

using 空间名::成员


💬 演示如何做到全都要:

#include <stdio.h>

namespace N1 {
	int a = 10;  // 假设a经常需要使用
	int b = 20;
	int c = 30;
}

void func(int n) {
	printf("HI, %d\n", n);
}

// using namespace N1;

using N1::a;  // 单独展开一个,其他的不展开

int main(void)
{
	 printf("%d\n", a); // 这样我们就可以直接使用了,就不需要 "::" 了
	 int res = a;
	 func(a);
	 printf("hello, %d\n", a);

	return 0;
}

  (既保持了方便,又保持了隔离,岂不美哉)



0x06 匿名命名空间

  为了能够安心享受极致的嘴臭,祖安人是非常喜欢匿名喷人的。

📚 我们在C语言学习结构体的时候,我们就提到过匿名结构体。

命名空间这里也可以匿名!如果一个命名空间没有名称,我们就称它为匿名结构体。

// 匿名命名空间
namespace {
	char c;
	int i;
	double d;
}

这种情况,编译器会在内部给这个没有名字的 "匿名命名空间" 生成一个惟一的名字。

并且还会为该匿名命名空间生成一条 using 指令,所以上面的代码会等同于:

namespace _UNIQUE_NAME {
	char c;
	int i;
	double d;
}
using namespace _UNIQUE_NAME;

这里我们只需要知道有这么一个东西就可以了,如果感兴趣可以深究下去。



0x07 使用方式总结 

学到这里,想必大伙已经对命名空间了解的差不多了,我们来总结一下命名空间使用的三种方式。

💬 方式一:

空间名 + 作用域限定符 

namespace N {
    int a = 10;
}

int main()
{
    printf("%d\n", N::a);

    return 0;
}


💬 方式二:

使用 using namespace 命名空间名称引入 (会破坏隔离效果)

namespace N {
    int a = 10;
}

using namespace N;

int main()
{
    printf("%d\n", a);

    return 0;
}


 💬 方式三:

使用 using 将命名空间中成员引入

namespace N {
    int a = 10;
}

using N::a;

int main()
{
    printf("%d\n", a);

    return 0;
}



Ⅳ.  C++的输入与输出

0x00 库的展开

  (我裂开了)

我们刚才之所以讲解命名空间,就是为了让大家能够慢慢地看懂本篇一开始写的 HelloWorld。

但是在讲解C++的输入与输出之前,我们还需要再对命名空间做一个小小的补充。

📚 库也是会用命名空间的,C++库的实现定义包含在了一个叫 std 的命名空间中。

我们加上 using namespace std 是为了把 std 空间中所有的内容都展开,这样我们就可以直接使用它们了。

❓ 为什么C++要把它封装到一个叫 std 的命名空间中呢?

💡 因为这样就不容易冲突了,有效放置了冲突命名。


📌 注意事项:这里要提一下,有些老的教材上有  #include <iostream.h>

这个在老的编译器上是可以的,比如 VC6.0 ,比较老的版本的库,没有命名空间。

早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件 即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文 件不带.h;旧编译器(vc 6.0)中还支持 <iostream.h> 格式,后续编译器已不支持,因此推荐使用 <iostream> +std 的方式。



0x01 输入和输出 

 cout 标准输出(控制台)和 cin 标准输入(键盘)时,必须包含 <iostream> 头文件。

并且需要使用 std 标准命名空间,这里我们下面会详细探讨。



0x02 流插入运算符 << 

  📚 cout 后面的 << ,我们称之为 "流插入运算符" 。就像水流的流向一样。



💬 cout 用法演示:

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello";  // 等同于 printf("Hello");

	return 0;
}

🚩 运行结果:  Hello

📜 上面的代码,我们之所以可以直接用 cout,是因为我们已经把 std 这块库命名空间展开了。我们下面会对专门对展开方式进行一个探讨。


📚 此外,如果我们想换行,我们可以使用 endl 。它就"相当于" \n,都可以起到一个换行的效果。



💬 endl 用法演示:

#include <iostream>

using namespace std;
int main()
{
	cout << "Hello" << endl;

	return 0;
}


当然,如果你不喜欢用 endl,完全可以用 \n 替代它:

#include <iostream>

using namespace std;
int main()
{
	cout << "Hello\n";  

	return 0;
}


📚 使用 C++ 的输入输出是很方便的,它最大的特点就是可以自动识别类型。

#include <iostream>
using namespace std;

int main()
{
	int i = 10;
	double d = 3.14;
	
	cout << i << " " << d << endl;   // 相当于 printf("%d %f\n", i, d);

	return 0;
}

🚩 运行结果: 10  3.14



0x03 流提取运算符 >>

📚 cin 后面的 >> ,我们称之为 "流提取运算符" 。

#include <iostream>
using namespace std;

int main()
{
	int i = 0;
	double d = 0.0;

	cout << "请输入一个整数和一个小数:> ";
	cin >> i >> d;

	cout << "你输入的是:> ";
	cout << i << " 和 " <<  d << endl;

	return 0;
}

🚩 运行结果如下:


❓ 可以改变流的流向吗?

💡 可以!

#include <iostream>
using namespace std;

struct Student {
    char name[20];
    int age;
};

int main(void)
{
    // cin - 流提取运算符
    struct Student s = {"xiaoming", 18};
    cin >> s.name >> s.age;
    cout << "姓名:" << s.name << endl;    
    cout << "年龄:" << s.age << endl << endl;
    
    scanf("%s%d", &s.name, &s.age);
    printf("姓名:%s\n年龄:%d\n", s.name, s.age);  

    return 0;
}


0x04 学会因地制宜

 我们好像可以在 C++ 里用 printf 欸!

#include <iostream>

int main()
{
	printf("Hello!\n");

	return 0;
}

因为 <iostream> 有些平台间接的包了 printf 等 C语言中的函数。


  有人这时候就会觉得,C++的输入和输出可以自动识别类型,这也太爽了吧,既然 C++ 的输入输出这么好用,那我们是不是就用不上 C语言里的输入输出了?



 并不是,我举个简单的栗子:

💬 如果我们想打印某个浮点数,如何控制小数点后的位数呢?在 C++ 的输入中,这是一件比较麻烦的事情。如果想控制小数点的位数,我们完全可以使用C语言的输入:

printf("%.2f", d);


因此,并不是说学了C++的输入输出,我们就不用C语言的输入输出了。

C++ 和 C语言的输入输出可以混在一起写,混在一起用。

什么时候用 C++ 的,什么时候用 C语言的,看情况就可以了。


💬 我们再来举个例子:

// 因地制宜!
struct Student {
    char name[20];
    int age;
};

int main(void)
{
    // 这种情况C++ 就不方便了
    struct Student s = {"xiaoming", 18};
    cout << "姓名:" << s.name << endl;    
    cout << "年龄:" << s.age << endl << endl;

    // 用c呢?
    printf("姓名:%s\n 年龄:%d\n", s.name, s.age);  
    // 所以说C语言也是有它的优势的嗷

    return 0;
}

🔺 总结:哪一个方便就用哪一个,具体谁方便需要看情况,要学会因地制宜。


0x05 对于展开方式的探讨

上面我们讨论过使用 using namespace 展开的缺陷,会失去隔离的效果。这样写好吗?不好。这么一来就全展开来了,所以我们可以这么写:

 (不裂开了,我合上)


💬 采用方式一:空间名 + 作用域限定符 

#include <iostream>
//using namespace std;

int main(void) 
{
    // cout << "Hello, World!\n" << endl; 
    std::cout << "Hello, World!" << std::endl;  // 指定命名空间

    return 0;
}


💬 采用方式二:使用 using namespace 命名空间名称引入 (会破坏隔离效果)

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello,World!" << endl;

	return 0;
}

📌 虽然 using namespace 会破坏隔离效果,但是我们平时写练习还是可以这么去做的。因为我们平时不需要这么过分地讲究命名空间,但是以后在写项目或干正事的时候就得讲究讲究了。


💬 采用方式三:使用 using 将命名空间中成员引入

#include <iostream>

using std::cout; // 把常用的展开
using std::endl;

int main(void)
{
    cout << "Hello, World!" << endl;  

    return 0;
}



Ⅴ.  再看 HelloWorld

深入浅出 —— 浅出

📚 现在我们再来回头看之前的 HelloWorld:

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello,World!" << endl;

	return 0;
}




参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. .

微软. public-api.wordpress.com[J]. , .


📌 笔者:王亦优

📃 更新: 2022.4.21

❌ 勘误: 无

📜 声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!


本章完。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。