这一期时关于C++的内容,从基础开始,大家都加油鸭!!!
文章目录
一.命名空间
1.1 namespace的价值
在C++里也有很多的变量(对象),函数和类。如果(变量,函数和类)的名称全部在全局作用域中,会产生冲突(比如在一个大型项目中,两个程序员分别完成两个部分的内容,有两个名称相同的变量名称,到底使用哪一个呢?)
使用命名空间的目的是对标识符的名称进行本地化,以避免(命名)冲突或名字污染,namespace
关键字的出现就是针对这种问题的。
1.2 namespace的定义
- 定义命名空间:namespace 空间的名字,后面再是花括号{}。和函数有点类似。
- 同一个域不能用同一个变量名,不同域可以用同一个变量名。
- namespace本质是定义一个域
- C++中域有函数局部域,全局域,命名空间域,类域(4个)。域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑[默认是在局部域main查找,之后会去全局域查找,不会主动去命名空间找],所以有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期,它们只是利用域做了隔离(在main函数里还可以用命名空间hou里面的变量,hou::rand)。
#include<stdio.h>
#include<stdlib.h> //注意,在这个库里有rand函数,在没有说明的情况下调用的是全局域里的
namespace hou
{
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
//调用命名空间里的函数
int a = hou::Add(1,2);
//原本是Add,现在是hou::Add(1,2)
//调用命名空间里的结构体
struct hou::Node bian;
//原本是struct Node bian; 注意hou::的位置
}
- namespace只能定义在全局,不能在main函数或者什么局部域里面定义。namespace只是为了和全局的内容做隔离,而局部域和全局的内容天然就是隔离的。
- namespace可以嵌套定义
namespace hou
{
namespace rong
{
int rand = 10;
}
namespace ning
{
int rand = 90;
}
}
int main()
{
printf("%d\n", hou::ning::rand);
}
- 项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
- C++标准库都放在一个叫std(standard)的命名空间中。
1.3 命名空间使用
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以我们要使用命名空间中定义的变量/函数,有三种方式:
- 指定命名空间访问(推荐推荐推荐)
//比如 hou::rand
- using将命名空间中的全部成员展开(但不推荐这种方式,会有命名冲突的风险)
using namespace hou;
//这样的话,找rand不仅会在全局域找,还会在hou里面找
- using将命名空间中某个成员展开(但不推荐这种方式,会有命名冲突的风险)
using hou::rand;
二. C++输入和输出
- iostream是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
- std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。(c是字符的意思)【只有在内存当中才有整型,浮点型,字符,原反补,而在其他设备:如文件,网络,终端控制台等等只认识字符】
- std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
- std::endl 是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。(endl----->end line结束行)
- <<是流插入运算符(<< 这个全名是流插入操作符,作用就是向控制台打印),>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)【可以自动识别变量类型,什么意思呢?就是不需要再写%d,%s之类的,<<可以自动识别】
- cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们。
#include<iostream>
int main()
{
std::cout << "hello world!"; //cout标准输出流
//如果想换行的话
std::cout << "hello world!"<<std::endl; //C++推荐这个方式
std::cout << "hello world!" << ' \n'; //可以连续的输出流插入,类型甚至可以不一样
}
- 这里我们没有包含<stdio.h>,也可以使用printf和scanf,在包含iostream时间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
三. 缺省参数
“缺省参数”又叫做”默认参数“,有参数,使用指定的参数;没有参数,使用缺省参数。(C语言没有缺省参数)
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。
缺省参数分为全缺省和半缺省参数。
-全缺省:全部形参给缺省值(即形参一个都没有,都使用默认值)。
-半缺省:传了部分形参,但还有一部分形参使用缺省值(默认值)。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
void function(int a=1, int b=2, int c=3)
{
printf("%d %d %d\n", a, b, c);
}
int main()
{
function(); //全缺省(全部形参使用缺省值)
function(11); //半缺省(没有传部分形参)
//function(,22,)这个是错误的,半缺省参数必须从右往左依次连续缺省
}
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。(声明是在.h(即目录)里的那个(
void function(ST* ps,int a = 10);
),定义是在.cpp的那个void function(ST* ps,int a){}
四. 函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同。(C语言不支持)
- 参数个数不同。
- 参数类型不同。
- 参数顺序不同。(本质还是参数类型不同)
- ‘返回值不同’不能作为重载条件,因为调用时也无法区分,不知道该调用哪一个。
// 1、参数类型个数不同
void f()
{
///
}
void f(int a)
{
///
}
f();
f(12);
// 2、参数类型不同
int Add(int left, int right)
{
///
}
double Add(double left, double right)
{
}
//调用时:
Add(10,20);
Add(1.1,2.2);
void f(int a, char b)
{
//
}
void f(char b, int a)
{
}
f(10,'m');
f('m',10);
错误:
// f()但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
//语法上不存在错误,但调用时会有问题
void f1()
{
/
}
void f1(int a = 10)
{
//
}
f1(); //有可能是第一个,也有可能时第二个,使用缺省值
五. 引用
5.1 引用的概念
引用:给已经存在的变量重起一个别名。(编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间)
类型& 引用别名 = 引用对象;
int a = 10;
int& b = a; //给a起一个别名b
区分typedef 和引用
typedef:给类型起别名
引用:给变量起别名
5.2 引用的特性
- 引用的对象必须是初始化过的。(引用在定义是必须初始化)
- 一个变量可以有多个引用。(可以有多个别名)
- 引用一旦引用一个实体,再不能引用其他实体。(专属别名)
5.3 引用的使用
引用的好处:
减少拷贝------>(1)提高效率;(2)改变引用对象(小号)的同时,改变被引用对象(本人)
引用—>没有开空间—>减少拷贝---->效率
- 引用传参:引用传参跟指针传参功能是类似的。
以前想改变a,b的值,需要把a和b的地址传过去,通过地址改变它们的值。
现在不需要这么麻烦,可以直接给a和b取个别名,将形参写成别名,改变别名即改变本人。
指针传参
引用传参
void change(int& aa,int&bb)
{ //aa是a的别名,bb是b的别名
//aa是引用对象,a是被引用的对象
aa = 1;
bb = 9;
}
int main()
{
int a = 10;
int b = 90;
change(a,b); //以a举例:a传递给形参int& aa的时候, 这个aa就是a的引用
std::cout << a << " " << b;
}
- 引用返回值
STTop()是一个返回值为int类型的函数,想让函数的返回值++,不能直接写STTop()++。
5.4 越界问题
越界访问不一定报错。
- 越界读,不报错。【比如数组a里有10个元素,cout<<a[10];就是越界读】
- 越界写,不一定报错。(越界写是以抽查的方式,越界的地方放一个值,比如-1,如果被改的话,就是越界写)【比如数组a里有10个元素,a[10]=33;就是越界写】
5.5 const引用
const int a = 10;
int& ra = a;
1.大家注意,这个方式是不可以的哈。可以引用const对象,但是必须也用const引用。
const int a = 10;
const int& ra = a;
2.const可以引用const对象,也可以引用普通对象。(对象的访问权限在引用过程中可以缩小,但是不能放大。对象是const int代表着这个a值不能被修改,那么你的别名一定也没有修改的权限)
//权限可以缩小
int b = 1; //b可以被修改
const int& rb = b; //b的别名rb,它的权限缩小了,不能被修改
//整个过程只影响rb的权限,不影响b的权限
//权限不可以被放大
//这个代码是不可以的,pa(即a的地址)不能被修改
//pb(也想存储a的地址),怎么可以让它是int*类型可以被修改呢
const int a = 10;
const int* pa = &a;
int* pb = pa;
const在*之前const int* pa=&a;
,修饰的是变量的内容。在 * 之后int* const pa=&a;
,修饰的是变量本身.
//错误示范
int a = 10;
const int* pa = &a; //pa的内容(a的地址)不能被修改
int* pb = pa;
//正确示范
int b = 99;
int* const pb = &b; //pb不能被修改(而不是pa指代的内容不能改),即pa里面存的内容一定b的地址
int* pbb = pb; //pb里放的内容和pa一样,但pb里的内容可以变
3.临时对象(const中的表达式)
int a = 9;
int& rb = a * 3;
首先先了解一下什么是临时对象?
临时对象就是编译器需要一个空间去暂存(表达式的求值结果)时,所临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象
a*3的值会先保存在临时对象里面,而C++规定临时对象具有常性(就像被const修饰一样),那么
int a = 9;
//int& rb = a * 3;
//表达式a*3被保存在临时变量里,被const修饰。而rb是int&,没有被const修饰。本质是权限变大的问题
//应该改成
const int& rb=a*3;
double a = 12.34;
const int& ra = a;
4. const引用的方便之处:
可以引用普通对象,const对象,临时对象
void func(const int& cc)
{
//将参数写做const int&是很方便的,调用函数时,既可以是int(缩小权限)
//又可以是const int&(权限没变)
//还可以是表达式 或者 double
}
int main()
{
int a = 0;
func(a);
const int b = 9;
func(b);
func(a * 3);
double d = 2.2;
func(d);
}
现在引用的都是比较小的参数,假如是4000多自己,传参的话需要临时拷贝,需要大量空间,这时引用就很厉害了。
5.6 引用和指针的区别
各有自己的特点,互相不可替代
- 引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间
- 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
- 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
- sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间(地址)所占字节个数(32位平台下占4个字节,64位下是8byte)
- 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些
六. inline
inline一般放在函数的前面,比如:
inline void print(){}
内联函数
• 用inline修饰的函数叫做内联函数,编译时,(C++编译器)会在调用的地方展开内联函数,这样调用内联函数就不用建立栈帧了,就可以提高效率。(正常情况下,调用函数是需要建立栈帧的)
inline对于编译器仅仅只是建议
• inline对于编译器而言只是一个建议(即使加了inline,编译器也可以选择在调用的地方不展开)【inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略】
替代宏函数
• C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
1. 宏
(宏有很多麻烦的点,但为什么还要用宏呢?因为宏有一个优点,可以直接替换。如果是调用函数,则需要建立栈帧)
宏函数缺点很多,但是替换机制,调用时不用建立栈帧,提效
//宏是C语言提供的三种预处理的功能之一
#define N 10 //不带参数的宏定义
//遇见N,就相当于是遇到10
- 宏的易错点
-
- 最后面不能加分号
-
- 最外面需要加括号
-
- 里面需要加括号
- 里面需要加括号
int Add1(int m, int n)
{
return m + n;
}
//如果没有外括号
#define Add1(a,b) (a)+(b)
int main()
{
std::cout << Add1(1, 2) * 3 << std::endl;
// std::cout << 1+2*3 << std::endl;
//运算符有优先级,*高于+,结果为7
}
/
int Add2(int m, int n)
{
return m + n;
}
//如果没有外括号
#define Add2(a,b) ((a)+(b))
int main()
{
std::cout << Add2(1, 2) * 3 << std::endl;
//则计算为((1)+(2))*3,这样会先计算1+2,再*3
}
int Add(int m, int n)
{
return m + n;
}
//如果里面没有括号
#define Add(a,b) ((a)+(b))
int main()
{
int x = 1, y = 2;
std::cout << Add(x&y, x|y) << std::endl;
// std::cout <<(x & y + x | y) << std::endl;
}
2.inline
没有宏函数的坑,还不用建立栈帧,提效。
inline int Add(int m, int n)
{
return m + n;
}
int main()
{
int x = 1, y = 2;
std::cout << Add(x&y, x|y) << std::endl;
return 0;
}
debug版本下默认不展开
• vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置以下两个地方。
inline的声明和定义不分开
• inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
七. nullptr
NULL实际是一个宏,在C语言里,NULL是0然后被强转为void*;C++里被定义为0
void f(int x)
{
std::cout << "f(int x)" << std::endl;
}
void f(int* ptr)
{
std::cout << "f(int* ptr)" << std::endl;
}
int main()
{
f(0);
//---->f(int x)
f(NULL);
//---->f(int x)
//本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。
f((int*)NULL);
//c++不允许void*转成任意类型的指针
//编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
f(nullptr);
}
C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。