本篇博客梳理C++入门基础知识~
一、命名空间namespace
1.namespace的价值:避免命名冲突
```cpp
#include <stdio.h>
#include <stdio.h>
int rand = 10;
int main()
{
// 编译报错:“rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
``
此处命名冲突:<stdlib.h>中包含了rand函数,程序里面又有一个rand变量,编译器不知道rand具体指的是什么
2.namespace的定义
(1)关键字:namespace,后面跟命名空间的名字,再接一对{}。可以定义变量/函数/类型等
(2)namespace的本质:定义一个域,有了域隔离,命名冲突就解决了
(3)namespace只可以定义在全局,支持嵌套定义——即命名空间里面还可以有命名空间
(4)工程上,多文件中的同名namespace会认为是同一个命名空间,不会冲突
(5)C++标准库都放在std命名空间中(standard)
#include <stdio.h>
#include <stdlib.h>
// bit是命名空间的名字,一般开发中是用项目名字做命名空间名。
namespace sp
{
int rand = 10;
int Add(int a, int b)
{
return a + b;
}
struct Node
{
struct Node* next;
int val;
};
namespace _sp//命名空间嵌套定义
{
int a;
int b;
}
}
int main()
{
// 这⾥默认是访问的是全局的rand函数指针
printf("%p\n", rand);
// 这⾥访问sp命名空间中的rand
printf("%d\n", sp::rand);
return 0;
}
::操作符:域作用限定符,上面程序就指定去sp命名空间里面找rand。
注:如果::左边没有东西,则默认访问全局,不会到命名空间里面去找
3.namespace的使用
要使用命名空间中定义的变量/函数时,有以下三种方式
(1)指定命名空间访问(项目中推荐此方式)
(2)using将命名空间的某个成员展开(项目中经常访问的不存在冲突的成员推荐此方式)
(3)展开命名空间中全部成员(比如using namespace std),项目中不推荐,日常练习可以使用
namespace N
{
int a = 0;
int b = 1;
}
// 指定命名空间访问
int main()
{
printf("%d\n", N::a);
return 0;
}
// using将命名空间中某个成员展开
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
// 展开命名空间中全部成员
using namespce N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
二、C++的输入与输出
1.iostream
全称Input Output Stream,定义了标准输入、输出对象
2.std::cin、std::cout和std::endl
(1)std::cin是istream类的对象,主要面向窄字符的标准输入流
(2)std::cout是stream类的对象,主要面向窄字符的标准输出流
(3)std::endl是一个函数,流插入输出时,相当于插入一个换行字符+刷新缓冲区
3.<<和>>:<<是流插入运算符,>>是流提取运算符
4.C++输入输出自动识别变量类型
不像C语言中的printf()和scanf()需要指定格式
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
cout << a << " " << b << " " << c << endl;
std::cout << a << " " << b << " " << c << std::endl;
}
三个不同类型的变量值a,b,c都可以正确输出
注:在输入输出需求较高的地方(比如输入很多东西的竞赛场景),加以下三行代码可以提高C++的IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
三、缺省参数(也叫默认参数)
1.缺省参数
声明或定义函数时给函数的参数定义一个默认值。调用函数时,如果没有指定实参,就用这个默认值
2.全缺省和半缺省
全缺省就是全部形参给默认值,半缺省就是部分形参给默认值。还有以下语法规定
(1)半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值
(2)当函数声明和定义分离(多文件场景)时,规定缺省值只能在函数声明里面给
- 全缺省和半缺省
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
// 半缺省
void Func2(int a, int b = 10, int c = 20)
- 函数声明和定义分离时,缺省值只能在函数声明里面给
// Stack.h
#include <iostream>
#include <assert.h>
using namespace std;
void STInit(ST* ps, int n = 4);//此处给了默认值
// Stack.cpp
#include"Stack.h"
// 缺省参数不能声明和定义同时给
void STInit(ST* ps, int n) //声明处给了默认值,定义这里就不能给了
{
assert(ps && n > 0);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
缺省参数的用处:需要的时候就传一个,不需要的时候用默认值就行。
四、函数重载
C++支持在同一作用域中出现同名函数,但要求这些同名函数的形参不同
1.参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
2.参数个数不同
void f()
{
//...
}
void f(int a)
{
//...
}
3.参数类型顺序不同
void f(int a, char b)
{
//...
}
void f(char b, int a)
{
//...
}
注意:返回值不同不能作为重载条件,因为调用时无法区分
五、引用
1.概念
给已经存在的变量取一个别名。类型&引用别名=引用对象
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
执行完之后a,b,c,d全部++,b c d本质都是a的别名
2.引用的特性
(1)引用在定义时必须初始化
(2)一个变量可以有多个引用
(3)引用一旦确定了指定的对象,所指对象就不变了,而指针可以修改其指向的对象
int a = 10;
int c = 20;
// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,
// 这⾥是⼀个赋值 (并非让b成为c的别名)
b = c;
3.引用的使用
(1)主要使用场景
①函数调用:引用传参和引用做返回值中减少拷贝,提高效率
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
Swap(x, y);
调用完Swap之后,x,y成功完成交换
注意:
1)引用做返回值的场景在类和对象中深入挖掘
2)指针变量也可以取别名
void ListPushBack(LTNode** phead, int x) ;
void ListPushBack(LTNode*& phead, int x) ;//此处不再需要二级指针,程序简化了
②改变引用对象时同时改变被引用对象(Swap函数就可以看出来)
(2)引用传参和指针传参功能对比
引用传参和指针传参功能类似(都可以通过函数调用改变实参),引用传参相对更方便一些
4.const引用
(1)引用常量的时候必须用const引用,但const引用也可以引用普通对象。原因:访问权限可以缩小但不能放大
如何理解权限缩小和权限放大:
比如这里有一个const int a = 0;
- 如果给a取个别名,这里的引用没有const修饰,就相当于a本尊可读不可写,替身反而变得可读+可写,这就叫权限的放大,是不允许的
再比如这里有个int a = 0; - 给a取个别名,这里的引用即便用了const修饰,相当于a本尊可读可写,替身可读不可写,这就叫权限的缩小,是允许的
总结一下就是,引用(替身)权限不能比本尊高
(2)临时对象:编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,这个对象就是临时对象
int main()
{
const int a = 10;
//int& ra = a;
// 编译报错: “初始化”: ⽆法从“const int”转换为“int &”
// 这⾥的引用是对a访问权限的放⼤,替身权力变大了
const int& ra = a;//(正确代码)
// 这⾥的引用是对b访问权限的缩⼩,替身权力变小了
int b = 20;
const int& rb = b;
return 0;
}
int main()
{
int a = 10;
const int& ra = 30; //允许给常量取别名,只是要const修饰
// 编译报错: “初始化”: ⽆法从“int”转换为“int &”
// int& rb = a * 3; a*3是临时对象,临时对象具有常性,不允许修改=>取别名要加const
const int& rb = a*3;
double d = 12.34;
// 编译报错:“初始化”: ⽆法从“double”转换为“int &”
// int& rd = d;
const int& rd = d; //d转成int时,会产生临时对象,有常性=>取名要加const
return 0;
}
5.指针和引用的关系(重要)
功能有重叠性,但有各自的特点,互相不可替代
(1)语法概念上:引用取别名是不需要开空间的,指针存的是变量的地址,要开空间
(2)引用在定义时必须初始化,指针建议初始化(语法上不必须)
(3)引用在初始化一个引用的对象之后,就不能再引用其它对象;而指针可以改变指向的对象
(4)引用可以直接访问对象,指针需要解引用才能访问对象
(5)sizeof(引用)==引用类型的大小,sizeof(指针)==地址空间所占字节个数
(6)引用相对安全(不会出现野指针,空指针的现象)
六、inline
1.内联函数
用inline修饰的函数。调用时C++编译器会在调用的地方展开内联函数,不需要额外建立函数栈帧,可以提高效率
2.inline对编译器只是一个建议
加了inline编译器也可以选择不展开。inline使用于频繁调用的短小函数,对于递归函数和代码量比较大的函数,加inline也会被编译器忽略
3.设计inline的目的
替代C的宏函数,宏写起来复杂且容易出错,而且只能有比较短的代码,不能实现较多样化的功能
4.inline最好不要声明和定义分离到两个文件
inline展开时没有函数地址,分离会导致链接错误,可以直接全都放.h里面,方便展开
七、nullptr
1.NULL的问题
NULL实际上是一个宏,在<stddef.h>中可以看到下面的代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C语言支持把void*指针隐式转成任意类型的指针,但C++不支持
像下面这样的代码,C语言支持,但C++不支持(因为C++检查更加严格)
void* p1 = NULL;
int* p2 = p1;
如果要这样调用函数:f(NULL),就会歧义,因为NULL被定义成0,调用到了上面的f(int* x)函数,跟我们本来想要调用的f(int* ptr)函数相违背,这就很不方便
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(NULL); // 本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),跟我们一开始程序的想法是不匹配的
f((int*)NULL); //这句代码可行
f((void*)NULL); //编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
f(nullptr); //这句代码可行
return 0;
}
2.nullptr介绍
根据上面举的例子总结起来就是,NULL在指针方面的应用是容易产生误会的,所以以后用nullptr表示空指针就好了。
C++11中引入nullptr关键字,它可以隐式转换成其它任意的指针类型,而不能被转换成整数类型。