🐇
🔥博客主页: 云曦
📋系列专栏:[C++]💨路漫漫其修远兮 吾将而求索 💛 感谢大家👍点赞 😋关注📝评论
文章目录
📚前言
在学习C语言时,我们经过一个很漫长的过程。但也是终于学过来了,从现在开始我们就此碰到了C++的门,但还进不去,因为C++是在C的基础之上的进阶学习,这也是一个很漫长的路程等待我们走完。活多说了下面我们就开始C++的入门学习吧!
📚1、命名空间
📔1.1、命名空间的定义
我们在学习C语言时,是否有过在写代码时偶尔会出现你定义的变量名与某个头文件的函数名命名冲突了,就只能无奈的去把变量名改了。这个问题其实在我们日常练习代码时,很少见,但是在我们做项目时却是一个很常见的问题。为此C++语言为了解决这个问题,命名空间就此诞生。
- 下面我们来看命名空间是如何定义的
//namespcae 为命名空间的关键字
//关键字后面是命名空间的名字
namespace xhy
{
}
//命名空间也支持嵌套
namespace xhy
{
namespace xhy1
{
namespace xhy2
{
}
}
- ___打个比方就是:有两片果园分别是野生的果园和舅舅家的果园,两片果园挨的很近,有一天你带着小伙伴想去野生的哪片果园摘果子吃,且野生的果园和舅舅家的又挨的很近,此时有个小伙伴误把舅舅家的果子摘了吃,之后舅舅看到了也不好怎么说,毕竟摘果子的是你的小伙伴。
___舅舅回去后想了为了不让别人误把自家的果园当成野生的果园摘果子,舅舅就给自家的果园围了一道墙,在此之后舅舅家的果园就没有被误摘过果子了。- 命名空间也是这个道理,把你的代码放在命名空间里相当于给你的代码围了一堵墙。
- 一个过程里可以有多个同名的命名空间,编译器会在最后合成同一个命名空间。
- 注意:一个命名空间就是一个新的作用域,命名空间里的所有内容就局限于命名空间内。
📔1.2、命名空间的使用
namespace xhy
{
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
int a = 10;
int b = 20;
Add(a, b);//编译报错:error C3861: “Add”: 找不到标识符
return 0;
}
因为Add函数在我们创建的命名空间里,所以我们访问不到Add函数。
- 访问命名空间的三中方式:
//第一种
//通过::域作用限定符的方式访问
int main()
{
int a = 10;
int b = 20;
// :: 是域作用限定符
xhy::Add(a, b);
return 0;
}
//第二种:
//using namespca 会把这个命名空间全部展开
//相当于原来的命名空间是有围墙的,展开后围墙就没了
//可以直接访问
using namespace xhy;
int main()
{
int a = 10;
int b = 20;
Add(a, b);
return 0;
}
//第三种
//using 指定命名空间的某个成员展开
using xhy :: Add;
int main()
{
int a = 10;
int b = 20;
Add(a, b);
return 0;
}
- 需要注意的是:第二种方法
using namespace
全部展开是有危险的,原本围起来就是为了防止命名冲突,这下好直接全部展开,命名空间等于摆设。- 但全部展开也不是没有用处:
- 我们在日常刷题、练习代码的时候是可以展开的
- 当我们在做项目时,就不要展开了,避免命名冲突
- 我们刚开始接触C++代码时,会发现多了一句代码不知道是什么意思
using namespace std;
,using namespace
我们在上面说过了这个是直接展开这个命名空间的用途,但std是哪个的命名空间呢?其实std是C++官方标准库的定义和实现都放在了这个命名空间里,如果在项目里展开大家可以想想会有什么风险!
📚2、C++的输入和输出
- 我们每接触一种新语言都先要学会用这个语言问候一下世界,那么让我们来看看C++是如何问候的呢?
#include<iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
- 讲解:
- cout 是C++的标准输出对象(控制台),使用前必须包含
#include<iostream>
头文件,以及命名空间std- << 是流插入运算符、endl就是C语言的 ‘\n’,只是C++把 \n弄成了关键字
- 介绍完C++的输出后我们再来看看C++的输入
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int b = 0;
cin >> a;
cin >> b;
//也可以这样写
cin >> a >> b;
return 0;
}
- 讲解:
- cin是C++的标准输入对象(键盘),和cout一样必须包含
#include<iostream>
头文件,以及命名空间std- >> 是流提取运算符,
- C++的输入输出更加方便,不像C语言的printf/scanf那样手动控制类型,C++的输入输出可以自动识别变量类型
#include<iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.00;
char ch = '\0';
//自动识别类型
cin >> a >> b >> ch;
cout << a << " " << b << " " << ch << endl;
return 0;
}
📚3、缺省参数
📔3.1、概念
缺省参数是在声明或定义函数时,为函数的参数给一个指定的缺省值,在调用该函数时,没有参数传入,就会用默认使用这个指定的缺省参数。
#include<iostream>
using namespace std;
void func(int x = 10)
{
cout << x << endl;
}
int main()
{
func();//没有参数传入时,会用知道的缺省参数
func(20);//有参数传入时,就会用传入的参数
return 0;
}
📔3.2、缺省参数的分类
- 全缺省参数
void func(int x = 1, int y = 1, int z = 1)
{
cout << x << y << z << endl;
}
- 半缺省参数
void func(int x, int y = 1, int z = 1)
{
cout << x << y << z << endl;
}
void func(int x, int y, int z = 1)
{
cout << x << y << z << endl;
}
- 注意:
- 半缺省函数必须从右往左依次给出,不能间隔着给缺省参数,这是不允许的。
//这样语法是不允许的
//因为不知道怎么给参数
void func(int x = 1, int y, int z = 1)
{
cout << x << y << z << endl;
}
void func(int x, int y = 1, int z)
{
cout << x << y << z << endl;
}
2.缺省参数不能在声明和定义同时出现
- 缺省值必须是常量或全局变量
- C语言不支持(编译器不支持)缺省参数
📚4、函数重载
📔4.1、概念
- 在学习C语言的时候,经常会遇到一种情况,就是在写Add函数的时候,当有不同类型的时候C语言只能这样写:
//函数不能同名只能这样写
int Addi(int x, int y)
{
return x + y;
}
double Addd(double x, double y)
{
return x + y;
}
- 但在C++可就不一样了,C++支持函数名可以有相同的。
- 函数重载:函数名相同,形参的(类型、参数个数、参数顺序)不同来进行重载
- 函数的返回值不参与重载
- 函数名相同,类型不同
int Add(int x, int y)
{
return x + y;
}
int Add(double x, double y)
{
return x + y;
}
- 函数名相同,参数的个数不同
int Add(int x, int y)
{
return x + y;
}
int Add(int x, int y, int z)
{
return x + y + z;
}
3.函数名相同,参数的类型顺序不同
int Add(double x, int y)
{
return x + y;
}
int Add(int x, double y)
{
return x + y;
}
📚5、引用
📔5.1、概念
- 所谓引用不是定义一个新的变量,而是给一个已经定义了的变量起一个别名。
- 类似西游记的猪八戒,在天庭时叫天蓬元帅,在凡间时叫猪八戒。虽然是两个名字但指的都是同一个人。
- 类型& 引用变量名(对象) = 引用实体;
#include<iostream>
using namespace std;
int main()
{
int a = 1;
//给a起了个别名叫b
//修改b就等同于修改a
int& b = a;
//可以打印地址出来看看
cout << &a << endl;
cout << &b << endl;
return 0;
}
- **注意:**引用的对象必须和实体是一个类型的。
📔5.2、特性
- 引用必须初始化。
- 一个变量可以有多个引用。
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int& b = a;
int& c = a;
int& d = a;
return 0;
}
- 引用一个实体后,不能再引用其他实体。
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int b = 2;
//这样就成重定义了
int& c = a;
int& c = b;
//这样是把b的内容拷贝给d
//所以引用一个实体后,就不能再引用其他实体了
int& d = a;
d = b;
return 0;
}
- 可以给别名起别名,类似孙悟空有一个齐天大圣的名字,但因为太长了所以就给齐天大圣起了个别名叫大圣。
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int& c = b;
int& d = c;
return 0;
}
5.3、📔常引用
#include<iostream>
using namespace std;
int main()
{
//1.数字是常量所以无法被引用?
int& a = 10;
//是否可以编译成功?
const int& b = 10;
int& c = b;
return 0;
}
- 解答:
#include<iostream>
using namespace std;
int main()
{
//1
//不可以,要引用一个常量必须在前面加const
const int& a = 10;
//2
//报错,因为一个只读的常引用给一个可读可写的引用属于权限放大
//把c加上const让c也成为常引用就可以运行了
const int& b = 10;
const int& c = b;
return 0;
}
📔5.4、引用的使用场景
- 做参数
#include<iostream>
using namespace std;
//C语言的交换函数
void Swap(int* left, int* right)
{
int tmp = *left;
*left = *right;
*right = tmp;
}
//C++的交换函数
//left是a的别名,right是b的别名
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 1;
int b = 2;
//C语言的传参
Swap(&a, &b);
//C++引用做形参的传参
Swap(a, b);
return 0;
}
- 做返回值
引用做返回值是有风险的,至于是什么风险,下面将会给大家讲解:
//大家可以看下这段代码是什么结果吗?
//A.正常运行 B.编译错误 C.运行崩溃 D.不确定
#include<iostream>
using namespace std;
int& Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 15;
int ret = Add(a, b);
cout << ret << endl;
return 0;
}
- 答案是A.正常运行。但输出的值是不确定的可能是25,也可能是随机值(要看编译器会不会清理栈)
- 当z返回时,是以引用返回的,那么接收它的值将是z的别名,但函数运行完时栈帧就会销毁(返回给操作系统),这时ret访问这个空间就出问题了,如果编译在栈帧销毁时清理了栈帧那么就是随机值,不清理就是返回的值。
- 栈帧都销毁了还能访问这个空间,你们说危不危险。
- 所以大家要注意:如果出了函数作用域,返回对象还在那么就用引用返回。出了函数作用域,返回对象销毁的就用传值返回即可。
📔5.5、引用和指针的区别
- 语法上引用就是给一个对象起别名,没有开空间
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = a;
return 0;
}
- 底层实现上是开了空间的,因为引用是按照指针的方式来实现的
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
b = 1;
int* p = &a;
*p = 10;
return 0;
}
- 引用与指针的不同点:
引用 | 指针 |
---|---|
引用概念上是给一个对象起别名 | 指针是存储一个变量的地址 |
引用在初始化时,引用一个实体后就不能再引用其他实体了 | 而指针可以在任何时候指向任何一个同类型的实体 |
没有NULL引用 | 但有NULL指针 |
sizeof的引用是取决于引用类型大小 | 但指针始终是地址空间,32位平台上是4字节、64位平台上是8字节 |
引用自增是给引用的实体加1 | 指针是向后偏移一个类型的大小 |
📚6、内联函数
- 大家在学习C语言的时候都学过宏定义吧。那么写一个加法函数来看看
#define ADD(x,y) (((x)+(y)))
- 宏定义细节多,如果不小心的话就会出现一些问题。
- 宏定义的缺点有:
- 无法调试
- 细节多,容易出错
- 宏定义的优点:
- 没有建立栈帧的开销
📔6.1、概念
- 以inline修饰的函数叫作内联函数,编译时C++编译器会在调用的地方展开函数,且没有建立栈帧的开销,内联函数提供了程序的运行效率。
- vs编译器的DeBug的模式下默认是不展开内联还是的,得去设置一下。
- 以下是VS2022的设置方式:
- 下面是内联函数展开后的汇编指令:
📔6.2、特性
- inline 是一种以空间换取时间的做法,如果编译器把函数当内联函数处理,在编译阶段,会用函数体替换函数调用。
- 缺点:可能会使目标文件变大。
- 优点:没有建立栈帧的开销,提高了程序的效率。
- inline对于编译器而言只是一种建议,会不会采用取决于编译器,不同的编译器实现可能会不同。(一般函数的行数在10以上都不会展开,但还是得看编译器)
- 内联函数不建议在多文件里声明和定义分离,分离会导致链接错误,因为内联函数展开时就没有函数的地址了(也就是内联函数的地址不会进链接阶段的符号表内),没有地址链接时就会找不到。
📚7、auto关键字(C++11)
📔7.1、auto简介
- 在早期的C/C++中,auto的含义是:使用auto修饰的变量,是具有自动存储器(寄存器)的局部变量,但很可惜的是没什么人用它。
- C++11中,标准委员会赋予了auto新的含义:auto不再是一个存储类型的指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译阶段推导而得。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
auto b = a;
char c = '\0';
auto d = c;
int* p = nullptr;
auto pa = p;
//auto e;//无法编译,使用auto定义变量时,必须进行初始化
//typeid(a).name()可以获取变量的类型
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(p).name() << endl;
cout << typeid(pa).name() << endl;
return 0;
}
- 看完上面的代码,想必大家也知道auto的作用了,auto的作用就是推导auto声明对象的类型。
- **注意:**auto声明的变量必须初始化,auto并不是一种 类型 的声明,而是一个类型声明的占位符,编译器在编译期间会把auto替换成实际变量的类型。
📔7.2、auto不能推导的场景
- auto不能做函数的参数。
//编译失败auto不能作为形参类型,
//因为编译器无法堆i的实际类型进行推导
void f(auto i = 0) {};
- auto不能直接声明数组。
#include<iostream>
using namespace std;
int main()
{
int arr1[] = { 1,2,3,4 };
auto arr2[] = { 2,3,4 };//err
return 0;
}
📔7.3、auto的使用规则
- auto声明变量时必须初始化。
- auto可以推导指针类型,用auto或auto*都可以,没有任何区别,但是auto引用必须加&。
#include<iostream>
using namespace std;
int main()
{
int* p = nullptr;
auto pa = p;
auto* pb = p;
auto& pc = p;
cout << typeid(pa).name() << endl;
cout << typeid(pb).name() << endl;
cout << typeid(pc).name() << endl;
return 0;
}
- 在同一行定义多个变量时必须都是同一个类型的,因为在同一行里auto只会推导一个类型。
#include<iostream>
using namespace std;
int main()
{
auto a = 1, b = 10;
auto c = 4, d = 4.0;//会编译失败,因为c和d不是一个类型
return 0;
}
📚8、基于范围的for循环(C++11)
📔8.1、 范围for的语法
- 我们先来看看C++98时,遍历数组的方法:
#include<iostream>
using namespace std;
int main()
{
int Arrer[] = { 1,2,3,4,5,6,7,8,9 };
int i = 0;
for (i = 0; i < sizeof(Arrer) / sizeof(Arrer[0]); i++)
{
cout << Arrer[i] << " ";
}
cout << endl;
return 0;
}
- 对于一个有范围的集合而言,由程序员来说明范围是多余的,也是还会容易犯错误。因此,C++11中引入了基于范围的for循环,for循环后的括号由冒号“”:“”分为两个部分,左边范围内用于迭代的变量,右边是被迭代的范围。
#include<iostream>
using namespace std;
int main()
{
int Arrer[] = { 1,2,3,4,5,6,7,8,9 };
//auto可以写其他类型,但auto好些,自动推导嘛
for (auto e : Arrer)
{
cout << e << " ";
}
cout << endl;
//想遍历改数组的内容,这时候引用就起很大作用了
for (auto& e : Arrer)
{
e += 1;
}
return 0;
}
📔8.2、范围for的使用条件
- 范围for循环的迭代范围必须是确定的。
- 迭代的对象必须要实现++和==的操作。(这个问题,会在STL篇,细说,现在只是了解一下)
📚9、C++98中的指针空值
- 我们在C语言中给一个指针置空是这样做的:
int* p = NULL
- 但在C++98中,NULL实际上是一个宏
- 这其实也是大佬们的一个失误,是人都会有失误的时候,能理解。
- 在C++11时,标准委员会用nullptr关键字弥补了这个漏洞。
#include<iostream>
using namespace std;
int main()
{
int* p = nullptr;
return 0;
}
- 注意:
- 在使用nullptr空值时,不需要包头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr)和sizeof(((void*)0))所占的字节是一样的。
- 为了提高代码的健壮性,后续给指针赋空值时建议使用nullptr。