1.命名空间
(1)命名空间的意义
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
(2)namespace定义
基本形式:
namespace 名称{
变量、函数、类的定义
}
代码示例:
#include <stdio.h>
#include <stdlib.h>
// 1. 正常的命名空间定义
// pig是命名空间的名字,⼀般开发中是⽤项⽬名字做命名空间名。
namespace pig
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
// 这⾥默认是访问的是全局的rand函数指针
printf("%p\n", rand);
// 这⾥指定bit命名空间中的rand
printf("%d\n", pig::rand);
return 0;
}
注意点:
- namespace只能定义在全局,可以嵌套定义。
代码示例
namespace animal
{
namespace pig
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
namespace dog
{
int rand = 2;
int Add(int left, int right)
{
return (left + right)*10;
}
}
}
int main()
{
printf("%d\n", animal::pg::rand);
printf("%d\n", animal::hg::rand);
printf("%d\n", animal::pg::Add(1, 2));
printf("%d\n", animal::hg::Add(1, 2));
return 0;
}
- 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,会合并到一起,不会冲突。
- C++标准库都放在⼀个叫std(standard)的命名空间中
(3)命名空间的使用
三种命名空间的使用方法:- 指定命名空间访问,项⽬中推荐这种⽅式。
- using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
- 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
三种方法的代码示例:
#include<stdio.h>
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;
}
2.C++输⼊&输出
形式:
输入:std::cout<<变量\常量\表达式\······<<endl;
输出:std::cin<<变量\常量\表达式\······<<endl;
注意点:
<iostream>
是Input Output Stream的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输
出对象。- std::cin是istream类的对象,它主要⾯向窄字符(narrow characters(of type char))的标准输⼊流。
- std::cout 是iostream类的对象,它主要⾯向窄字符的标准输出流。
- std::endl是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区
- <<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
- C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载),C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
- ⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
- 没有包含<stdio.h>,也可以使⽤printf和scanf,在包含
<iostream>
间接包含了。vs系列
编译器是这样的,其他编译器可能会报错。 - 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,可以加上以下3⾏代码
#include<iostream>
using namespace std;
int main()
{
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
3. 缺省参数
- 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参,则采⽤该形参的缺省值,否则使⽤指定的实参。
- 缺省参数分为全缺省和半缺省参数,全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
代码示例
//采用形参缺省值
#include <iostream>
#include <assert.h>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func();// 没有传参时,使⽤参数的默认值
Func(10);// 传参时,使⽤指定的实参
return 0;
}
输出结果:
0
10
#include <iostream>
using namespace std;
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//错误,必须依次从右往左缺省
//void Func2(int a=10, int b, int c)
//{
// cout << "a = " << a << endl;
// cout << "b = " << b << endl;
// cout << "c = " << c << endl << endl;
//}
int main()
{
Func1();
Func1(1);
Func1(1,2);
Func1(1,2,3);
Func2(100);
Func2(100, 200);
}
输出结果:
a = 10
b = 20
c = 30
a = 1
b = 20
c = 30
a = 1
b = 2
c = 30
a = 1
b = 2
c = 3
a = 100
b = 10
c = 20
a = 100
b = 200
c = 20
4. 函数重载
(1)定义
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
(2)函数重载的多种形式
- 函数参数不同
- 参数个数不同
- 参数类型顺序不同
注意:返回值不同不能作为重载条件,因为调⽤时也⽆法区分
#include<iostream>
using namespace std;
// 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()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
运行结果:
int Add(int left, int right)
double Add(double left, double right)
f()
f(int a)
f(int a,char b)
f(char b, int a)
5.引用
(1)引用的概念和定义
引用不是新定义⼀个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同⼀块内存空间。
如图所示,a和b有着同样的地址和值
类型& 引用别名=引用对象;
在这里面,a,b,c,d 共用一个地址空间
(2)引用的特性
-
引用在定义时必须初始化
如图所示,不进行初始化就会发生报错
-
⼀个变量可以有多个引用
a,b,c 有着同样的值
当我想通过b=c,引用第二个实体时,发现b的地址仍然是a的地址,由此我们知道引用不能引用多个实体
(3)引用的使用
- 引用传参和引用做返回值
优点:1.不开空间,提高效率2.改变引用对象时同时改变被引用对象
引用传参
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main(){
int a = 5;
int b = 10;
swap(a, b);
cout << "a=" << a << "\n" << "b=" << b << endl;
}
输出结果为a=10,b=5
从此可得,swap函数通过引用传参可以直接通过改变形参来改变实参
值的临时变量做返回值
当我想直接对函数返回的只进行修改时出现报错,这是因为函数StackTop1函数返回了一个常性的临时变量,对它直接赋值会产生报错
引用做返回值
但注意不要把临时变量传给引用,这样会产生空引用
当这个函数执行之后,top会被销毁,此时StackTop3就发生了空引用
(4)const引用
对于const引用只需要记住一句话——避免权限扩大
什么是权限放大,简单来说就是,每个变量有读和写两种权限,当引用有读和写两种权限,被引用的常量只有一种读的权限时就发生了权限扩大
int main(){
//一般引用=常量
const int a = 3;
//int& ra = a;//权限放大出现报错
const int& ra = a;
// 一般引用=临时变量
//由表达式产生的临时变量
int b = 3;
int c = 4;
//int& rb = a+b;//发生报错,a+b产生临时变量,只能读不能写
const int& rb = a+b;
//隐式转换产生的临时变量
double d = 12.44;
//int& rc = d//发生报错,d隐式类型转换产生临时变量,只能读不能写
const int& rc =b;
}
const引用有以下几种用法:
- 权限平移
常引用=常量
int main(){
const int a = 3;
const int& ra = a;
int b = 3;
int c = 4;
const int& rb = b+c;
double d = 12.44;
const int& rc =b;
}
a本身只能读不能写,将其传给const引用ra后,ra也只能读取它们共有地址的值,而不能进行修改
b+c表达式会产生一个临时对象只能读不能写,只能传给const引用rb
d隐式类型转换产生临时对象,只能读不能写,只能传给const引用rc
注意:临时对象在被传给const引用后,寿命跟着引用走,不用担心临时对象销毁从而引用被销毁
2. 权限缩小
常引用=变量
int main(){
int a = 3;
const int rb = a;
}
a可读可写,将其传给const引用ra后,ra只能读取它们共有地址的值,不能进行修改,权限缩小
通过上面const引用的用法,我们可以发现,const引用的使用是为了防止有时候普通引用不适用的情况
如果我想调用这个函数,如果我不用const引用接收
那么我下面的写法就会产生报错
list a ;
a.push_back(1);
a.push_back(2);
a.push_back(3);
(5)指针和引用的关系
方面 | 引用 | 指针 |
---|---|---|
语法上 | 变量别名,不开空间 | 存储一个变量地址,需要开空间 |
是否需要初始化 | 必须初始化 | 不一定要初始化 |
能否改变对象 | 不能改变引用对象 | 可改变指向对象 |
访问方式 | 直接访问 | 解引用后才能访问 |
安全性 | 比较安全 | 相对危险,容易产生野指针 |
sizeof大小 | 引用类型的大小,因类型而改变 | 空间一定,始终为地址空间所占的字节数 |
从汇编代码可以看出,在底层逻辑上,指针和引用有着相同的逻辑
6.inline
⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联 函数就需要建⽴栈帧了,就可以提⾼效率。内联函数存在的意义是代替C中的宏函数,因为宏函数的定义是在很繁琐。
我们将a+b定义成宏函数:
#define ADD(a,b) ((a)+(b))
以下是该宏函数定义的易错点:
//在末尾加分号
#define ADD(a,b) ((a)+(b));//cout<<ADD(a,b)<<endl;等效cout<<((a)+(b));<<endl;
//外面括号丢失
#define ADD(a,b) (a)+(b)//ADD(a,b)*5等效(a)*(b)*5
//里面括号丢失
#define ADD(a,b) (a+b)//ADD(x&y,x|y)等效为(x&y+x|y)而加的优先级大于&和|
从此看出宏函数十分麻烦易错,因此C++用内联函数来代替它
debug调试下默认不展开inline,可以通过以下方法来设置
将上面的宏函数转化为内联函数如代码所示,可以看出确实比宏函数明了很多
inline int ADD(int a,int b ) {
return a + b;
}
int main() {
int a = 1;
int b = 2;
int c = ADD(a , b);
}
汇编代码如下(暂时先不做解释,之后对汇编有较深理解了再补吧…)
但是当内联函数足够长和复杂时,编译器不会展开内联函数
inline int ADD(int a,int b ) {
a++;
a++
b++
b++
return a + b;
}
int main() {
int a = 1;
int b = 2;
int c = ADD(a , b);
}
注意:不建议将内联函数的定义和声明分别定义到两个文件中,直接放到.h文件中
7.nullptr
在学习NULL之前,让我们先回顾一下C语言中的NULL
在C语言中,NULL是一个宏
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
从这个定义我们可以知道NULL有两种可能,一个是整型0,一个是空指针,就是因为有两种可能,会在可以进行函数重载的C++中遇到麻烦。
假设我们有一个函数fun该函数定义如下:
//C++
void fun(int x) {
x++;
cout << x;
}
void fun(int* x) {
if (x == NULL) {
cout << "x为空指针";
}
}
int main() {
fun((void*)NULL);//发生报错
}
//C
void fun(int* x) {
if (x == NULL)
printf("x为空指针");
}
int main() {
fun((void*)NULL);//正常运行(不用显式转换也行)
}
当我们用fun(NULL)
调用fun时,编译器并不知道我们想调用的是第一个fun还是第二个fun,而产生错误调用,这时你可能会说用fun(int* x)
,但因为C++的隐式转换比C更加严格,对于指针类型,C++需要显式的类型转换才能从void*
转换为其他具体类型的指针,此时 void*
并不能隐式转换为int*
从而调用第二个函数。
说了这么多,只需要得出一个结论即可:NULL在能重载的C++中并不适用,需要我们使用nullptr
C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换
成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被
隐式地转换为指针类型,⽽不能被转换为整数类型
void fun(int x) {
x++;
cout << x;
}
void fun(int* x) {
if (x == NULL) {
cout << "x为空指针";
}
}
int main() {
fun(nullptr);//正常输出"x为空指针"
return 0;
}