C++入门基础(1)
目录
- 前言
- 1.C++的第一个程序
- 2. 命名空间
- 2.1 namespace的价值
- 2.2 namespace的定义
- 2.3 命名空间使用
- 3. C++输入&输出
前言
下面这张图是C++的发展历程:
这里我们不再对C++的历史进行具体的介绍,我们直接进入正题!
1.C++的第一个程序
C++我们的重点学习部分是类和对象,但是这里我们先学习一些C++小的语法。我们的C++是兼容C语言的,我们的C语言中的语法基本上可以在C++中使用,当然会有个别的差异,以后我会提到。
C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行,如下:
当然C++有⼀套自己的输入输出,严格说C++版本的hello world应该是这样写的:
这里我们可以看到与我们之前学习的C语言有很大的不同,现在进行具体的讲解。
2. 命名空间
2.1 namespace的价值
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
这一都段文字的意思可以理解为一个域里面不可能存在两个名字一样的人,但是两个不同的域中可以出现两个名字一样的人。
c语言项目类似下面程序这样的命名冲突是普遍存在的问题,C++引入namespace就是为了更好的解决这样的问题。
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:“rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
这里我们需要打印出rand的值,但是这里的rand实际上是有两个的一个在全局变量中另一个在头文件中,在进行编译时就会因为命名冲突而报错。
2.2 namespace的定义
这里进行一个小的总结,之后我会对其中的内容进行详细的讲解。
- 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{ }即可,{ }中
即为命名空间的成员。命名空间中可以定义变量/函数/类型等。 - namespace本质是定义出⼀个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不在冲突了。
- C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/
类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响
编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。 - namespace只能定义在全局,当然他还可以嵌套定义。
- 项目工程中多文件中定义的同名namespace会认为是⼀个namespace,不会冲突。
- C++标准库都放在⼀个叫std(standard)的命名空间中。
先具体看看前面的两点,我为了防止出先之前的命名冲突,我这里使用namespace划出了一个域,在这个与里面,我们定义了一些函数或者什么变量之类的,可能这些东西的名称在头文件中有一样的,但是这样并没有违法同一个域里面不能有同名变量的规则。
之前我在C语言中提到的比较多的地方就是局部域和全局域,在一些小的循环中,往往会有一些比较小的局部域。编译器的特点就是我们在使用一些函数和变量的时候,我们需要先找到这些函数或者变量的声明或者定义,可以这样进行简单的理解比如我在写一篇论文的时候我所引用的内容需要可以找到相应的出处。没有进行特殊指定的情况下,在进行查找的时候,首先在局部域进行查找再到全局域进行查找。
在理解了这些内容之后,对与前面的代码我们可以有进一步的理解,printf里面的变量rand进行使用的时候我们需要进行查找,在局部域之中并没有找到,到全局域里面进行查找的时候,有两个rand,一个是函数一个是变量,编译器并不知道使用哪一个的时候就会报错。
如果我们采用一下的代码就会找到stdlib里面的rand函数并打印相应的地址。
#include <stdio.h>
#include <stdlib.h>
namespace sz
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
printf("%p\n", rand);
return 0;
}
这里我们不难看出默认先在局部域中进行查找,再到全局域里面进行查找,并不会直接到我们的命名空间域里面去查找的。
在一开始的综述中,我提到域影响的是编译器的查找规则。除此之外,我们的域还会变量的影响生命周期,但是我们的命名空间域和类域不会影响生命周期,类域在以后会进行讲解。
具体用处的话,可以进行这样的理解,假设我和张三在写代码的时候都写了一个Func函数,单独进行运行没有什么问题,但是我们的代码一起提交之后,就会出现重命名,我和张三就可以用两个命名空间域把我们的Func函数分别放进去就可以解决这样的问题。
如果要对命名空间域里面的rand进行访问的话,我们需要::(域作用限制符)进行访问,如下:
命名空间只能定义在全局,不能定义在局部,如下:
int main()
{
//err
namespace nh
{
int rand = 99;
}
/*printf("%p\n", rand);
printf("%d\n", sz:: rand);*/
return 0;
}
namespace是可以嵌套定义的。我们可以通过下图进行理解:
可以这样理解:这里有两杯奶茶,我们为了防止它串味,分别用两个杯子进行容纳,但是每一杯奶茶里面又有不同的小料,比如珍珠和布丁。但是依旧不影响生命周期。
如果想要具体使用的话可以参考如下代码:
#include <stdio.h>
#include <stdlib.h>
namespace bit
{
// zhangsan
namespace zs
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
// 李四
namespace lisi
{
int rand = 2;
int Add(int left, int right)
{
return (left + right) * 10;
}
}
}
int main()
{
printf("%d\n", bit::zs::rand);
printf("%d\n", bit::lisi::rand);
printf("%d\n", bit::zs::Add(1, 2));
printf("%d\n", bit::lisi::Add(1, 2));
return 0;
}
对于结构体要注意一下,我们这样进行使用:
#include <stdio.h>
#include <stdlib.h>
namespace bit
{
// zhangsan
namespace zs
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
// 李四
namespace lisi
{
int rand = 2;
int Add(int left, int right)
{
return (left + right) * 10;
}
struct Node
{
struct Node* next;
int val;
};
}
}
int main()
{
printf("%d\n", bit::zs::rand);
printf("%d\n", bit::lisi::rand);
printf("%d\n", bit::zs::Add(1, 2));
printf("%d\n", bit::lisi::Add(1, 2));
struct bit::lisi::Node node;
return 0;
}
我们也会遇到有很多文件的情况,假设我这里定义一个栈,我会担心我的栈与其他同学的栈冲突。
假设我在我的Stack.h里面把函数声明用命名空间域分装一下,我把他称为nh,同样的我在Stack.cpp里面用相同名称的命名空间域把我的函数分装一下,这样是不会发生冲突的。
Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
namespace nh
{
typedef int STDataType;
//定义栈的数据结构
typedef struct Stack
{
STDataType* arr;
int top; //指向栈顶位置
int capacity; //容量
}ST;
void STInit(ST* ps);
void STDestroy(ST* ps);
//入栈--栈顶
void StackPush(ST* ps, STDataType x);
//出栈--栈顶
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//栈是否为空
bool StackEmpty(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
}
Stack.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
namespace nh
{
void STInit(ST* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
void STDestroy(ST* ps)
{
if (ps->arr != NULL)
free(ps->arr);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//入栈--栈顶
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
//空间满了--增容
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
//空间足够
ps->arr[ps->top++] = x;
}
//栈是否为空
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
//出栈--栈顶
void StackPop(ST* ps)
{
assert(!StackEmpty(ps));
--ps->top;
}
//取栈顶元素
STDataType StackTop(ST* ps)
{
assert(!StackEmpty(ps));
return ps->arr[ps->top - 1];
}
//获取栈中有效元素个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
}
一个文件或者多个文件里面同名的命名空间会自动合并为一个。
C++当然也怕自己的东西和其他人的冲突,所以C++标准库都放在一个叫std(standard)的命名空间中。
我们的hello world也可以这样写:
2.3 命名空间使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以
下面程序会编译报错。所以我们要使用命名空间中定义的变量/函数,有三种方式:
- 指定命名空间访问,项目中推荐这种方式。
- using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
- 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
这里的第一条我们刚才已经讲过了。但是有的时候大家可能会觉得这个会比较麻烦,并且不怕或者确定没有冲突,可以使用命名空间展开,这个展开和头文件的展开不一样,头文件的展开是把头文件的代码拷贝一份放过去,命名空间的展开是在编译的时候不仅在局部和全局查找,还在命名空间中进行查找。
当然还有折中的方法进行部分展开,如下:
3. C++输入&输出
-
< iostream > 是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
-
std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。
-
std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
-
std::endl 是⼀个函数,流插入输出时,相当于插入⼀个换行字符加刷新缓冲区。
-
<<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
-
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是 C++的流能更好的支持自定义类型对象的输入输出。
-
IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识⼀下C++IO流的用法,后面我们会有专门的一个章节来细节IO流库。
-
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要
通过命名空间的使用方式去用他们。 -
⼀般日常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
-
这⾥我们没有包含<stdio.h>,也可以使用printf和scanf,在包含 < iostream > 间接包含了。vs系列
编译器是这样的,其他编译器可能会报错。
这里我进行了总的论述,对于相关的具体内容需要进行之后的学习才能进一步的理解。C语言里面是有输入输出的,但是这个是有缺陷的,所以在C++中这里有具体的修改。
第五条那里的两个符号大家可以理解为输入和输出。
我们可以看到这里它是自动识别类型的,在C语言中我们往往要写%d等来匹配。
如果需要换行,可以采用如下方式:
#include<iostream>
using namespace std;
int main()
{
int i = 0;
double d = 1.1;
cout << i << '\n' ;
cout << d;
return 0;
}
上面我们提到了那个endl它在效果上可以视为和我们写的换行一样,但是它本身比较复杂,可以这样简单理解。当然输入也可以,依照下面的方法:
如果想要指定小数的位数,我们可以使用C语言,printf这里指定一下。C++在这方面比较复杂。
还有头文件这里,我们的.h是不用写的,当然一些很老的版本支持,但是一般不考虑。
最后,如果你要去打比赛之类的话可以加上下面的代码,因为当测试用例比较多的时候,我们的cout和cin的效率是不如scanf和printf的。
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
好了,我们的C++知识就讲到这里。如果文章内容有误,请大佬在评论区斧正!谢谢大家!