C++第一讲:开篇
这一讲是学习C++的第一讲,以后会一直学习C++,后序所写的算法题也将会使用C++来写,所以这一讲叫做开篇,讲述的是C++的基础知识
1.C++历史背景
关于C++的历史背景,了解与否其实无关紧要,但是这里还是稍微讲讲有意思的事情吧,毕竟学习语言首先还是要先了解一下它的历史
1.1C++创世主--本贾尼
C++的起源可以追溯到1979年,当时Bjarne Stroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不同的地⽅可能有差异)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作。他在实现项目的开发工作时,感受到了C语言在一些方面的不足,1983年,于是他在C语言之上添加了面向对象的特性,也是在这一年该语言被命名为C++
仰望大佬:
1.2C++版本更新
1.3C++的重要性
C++的重要性想必也不用过多赘述了,学习C++的人应该都能够体会到C++的重要性,无论是在工作中、学习中
1.4C++书籍推荐
下面的三本书是大佬推荐的书,至于怎么样,我相信,到后边我会看的!
2.C++的第一个程序
c
//第一个C++程序
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
这里我们是不是还看不懂这些代码?没关系,我们往下看
3.命名空间
3.1namespace是什么
在C语言中,我们不能使用同一个名称定义多个变量:
c
#include <stdio.h>
//在只引用stdio这一个头文件的情况下,这串代码是正确的
int rand = 10;
int main()
{
printf("%d ", rand);
return 0;
}
那么下面我们改一下:
c
#include <stdio.h>
#include <stdlib.h>
//在引用stdlib头文件之后,就发生了报错:rand重定义,以前的定义是"函数"
//原因:引用头文件其实就是在链接过程将头文件内容拿过来,而stdlib这一头文件中
// 也包含rand,而且它还是一个函数,所以这里就报错了
int rand = 10;
int main()
{
printf("%d ", rand);
return 0;
}
这在以后多人项目中是非常拉跨的,因为我们不能保证整个团队定义的变量都是不同的名称,所以C++中就引入了namespace来解决这个问题
3.2namespace的使用
总结:
1.namespace是定义命名空间的关键字,命名空间中可以包含变量、函数、结构体等
2.在C++中,两个冒号::被称为是作用域解析运算符,它用来指定某个实体(变量、函数、类)属于哪个作用域或命名空间
3.namespace本质就是定义了一个命名空间,这个域跟全局域相互独立,不同的域可以定义同名变量,所以这里的rand就不会报错了
4.C++中有函数局部域、全局域、命名空间域、类域,编译器在编译时会先在局部域中寻找变量,然后再在全局域中寻找变量。局部域和全局域除了会影响编译查找逻辑,还会影响变量的声明周期,命名空间域和类域不会影响变量声明周期
1.namespace是定义命名空间的关键字,命名空间中可以包含变量、函数、结构体等:
c
#include <stdio.h>
#include <stdlib.h>
//namespace定义命名空间,定义方法如下:
//namespace+命名空间名称--自己起--BCNR--爆炒脑仁
namespace BCNR
{
//定义变量
int rand = 10;
//定义函数
int ADD(int x, int y)
{
return x + y;
}
//定义结构体
struct Stack
{
int* arr;
int capacity;
int top;
};
}
int main()
{
printf("%p\n", rand);
//使用命名空间中对象的方法:命名空间名称 + :: + 对象名称
//提取变量
printf("%d\n", BCNR::rand);
//使用空间中的函数
int c = BCNR::ADD(10, 20);
printf("%d\n", c);
//使用结构体
BCNR::Stack st1;
return 0;
}
2.在C++中,两个冒号::被称为是作用域解析运算符,它用来指定某个实体(变量、函数、类)属于哪个作用域或命名空间
3.namespace本质就是定义了一个命名空间,这个域跟全局域相互独立,不同的域可以定义同名变量,所以这里的rand就不会报错了
4.C++中有函数局部域、全局域、命名空间域、类域,编译器在编译时会先在局部域中寻找变量,然后再在全局域中寻找变量。局部域和全局域除了会影响编译查找逻辑,还会影响变量的声明周期,命名空间域和类域不会影响变量声明周期
c
#include <stdio.h>
//定义在全局域
int a = 10;
namespace BCNR
{
//定义在命名空间域
int a = 20;
}
//函数局部域
int ADD(int x, int y)
{
return x + y;
}
int main()
{
//定义在函数局部域
int a = 30;
printf("%d\n", a);//main函数局部域中,30
printf("%d\n", BCNR::a);//命名空间域中,20
printf("%d\n", ::a);//像这样只有两个冒号,就是访问全局域中的变量,10
return 0;
}
3.3namespace使用注意事项
总结:
1.namespace可以嵌套使用
2.namespace只能定义在全局
3.项目工程中定义的多个同名namespace会认为是一个namespace,不会冲突
4.C++标准库都放在一个叫std(standard)的命名空间中
5.namespace创建的命名空间中的对象其实都是全局的,因为这个变量能够在非命名空间域中被访问
1.namespace可以嵌套使用:
c
#include <stdio.h>
//假设此时张三和李四要实现一个项目:Game
namespace Game
{
//张三
namespace ZhangSan
{
int a = 10;
int ADD(int x, int y)
{
return x + y;
}
}
//李四
namespace LiSi
{
int a = 20;
int ADD(int x, int y)
{
return x + y * 10;
}
}
}
int main()
{
int ret;
//张三
printf("%d\n", Game::ZhangSan::a);//10
ret = Game::ZhangSan::ADD(10, 10);
printf("%d\n", ret);//10+10=20
//李四
printf("%d\n", Game::LiSi::a);//20
ret = Game::LiSi::ADD(10, 10);
printf("%d\n", ret);//10+10*10=110
return 0;
}
2.namespace只能定义在全局
3.项目工程中定义的多个同名namespace会认为是一个namespace,不会冲突
c
//同名namespace会合并在一起
//Stack.h
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace bit
{
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps, int n);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
STDataType STTop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
}
// Stack.cpp
#include"Stack.h"
namespace bit
{
void STInit(ST* ps, int n)
{
assert(ps);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
// 栈顶
void STPush(ST* ps, STDataType x)
{
assert(ps);
// 满了, 扩容
if (ps->top == ps->capacity)
{
printf("扩容\n");
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity
* 2;
STDataType* tmp = (STDataType*)realloc(ps->a,
newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
}
4.C++标准库都放在一个叫std(standard)的命名空间中
c
#include <iostream>
int main()
{
//既然C++标准库都放在了std这一命名空间中,那么我们在引用了
//头文件之后就可以对其中的对象进行使用了:
std::cout << "hello world!" << std::endl;
return 0;
}
5.namespace创建的命名空间中的对象其实都是全局的,因为这个变量能够在非命名空间域中被访问
3.4命名空间的使用
上述我们已经了解到,双冒号可以拿出指定命名空间中的变量来使用,如果我们要一直拿出某个变量的话,敲代码是一件很麻烦的事,所以我们这样做:
c
#include <stdio.h>
#include <stdlib.h>
namespace BCNR
{
//定义变量
int a = 10;
//定义函数
int ADD(int x, int y)
{
return x + y;
}
}
//如果我们要像下面多次使用a的话,是很麻烦的,那么我们加上这个:
//此时代表着展开BCNR这一命名空间中的a变量,将a变量暴漏在全局
using BCNR::a;
//这样代表着将命名空间中所有的对象暴漏在全局中
using namespace BCNR;
int main()
{
printf("%d\n", a);
printf("%d\n", BCNR::a);
printf("%d\n", BCNR::a);
printf("%d\n", BCNR::a);
printf("%d\n", BCNR::a);
printf("%d\n", BCNR::a);
printf("%d\n", BCNR::a);
printf("%d\n", BCNR::a);
return 0;
}
现在我们就可以理解C++的第一个代码中的using namespace std是什么意思了
4.C++输入和输出
1.iostream是标准输入流和标准输出流的一部分,它提供了对标准输入和标准输出的访问,是istream和ostream的组合,istream表示输入流,用于从输入流(如键盘)中读取数据,ostream为输出流,用于向输出流(如屏幕)中输出数据
2.std::cin,是istream类的对象,用于从标准输入流中读取数据
3.std::cout,是ostream类的对象,用于向标准输出流发送数据
4.std::enl,它是一个函数,而且是一个较为复杂的函数,相当于插入一个换行符并刷新缓冲区
5.<<是流插入运算符,>>是流提取运算符
6.与C语言不同的是,C++中的输入输出可以自动识别数据类型,不需要使用%d、%c来格式化输入、输出
7.IO流中设计很多类和对象、重载中的知识,后边会详细阐述
8.cout、cin、endl等都属于C++标准库,C++标准库都放在一个叫std的命名空间中,所以要通过命名空间的使用方法来使用它们
9.日常练习中,我们可以用using namespace std,在项目中万不可这样使用
10.仅仅包含iostream,我们也可以使用printf、scanf,可能是在iostream中间接包含了,VS没有报错,其他编译器可能会报错,这也说明了IO流不仅仅是各司其职的,在IO需求较高的情况下,比如竞赛中,可能会因为IO流之间的关系处理而浪费时间,解决方式如下:
c
#include <iostream>
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
//会自动识别数据类型进行打印
//如果想要保留小数的话,建议使用printf来实现小数的保留
std::cout << a << " " << b << " " << c << std::endl;
//可以⾃动识别变量的类型
std::cin >> a;
std::cin >> b >> c;
std::cout << a << std::endl;
std::cout << b << " " << c << std::endl;
return 0;
}
#include<iostream>
using namespace std;
int main()
{
//在IO需求⽐较⾼的地⽅,如部分⼤量输入的竞赛题中,加上以下3⾏代码
//可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
5.缺省参数
缺省参数其实就是在函数声明时,为函数参数定义一个缺省值,如果传入的数据没有值的话,就会使用缺省值:
c
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); //没有传参时,使⽤参数的默认值,输出0
Func(10); //传参时,使⽤指定的实参,输出10
return 0;
}
缺省分为全缺省和半缺省
c
#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 = 10, int b, int c = 20)//err,缺少实参2的默认实参
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func1();
Func1(1);
Func1(1, 2);
//在传参时也不能跳跃着传参
//Func1(, 1, );//err,语法错误
Func1(1, 2, 3);
Func2(100);
Func2(100, 200);
Func2(100, 200, 300);
return 0;
}
需要注意的是,缺省参数不能声明和函数同时给值,只能够在声明中确定缺省值
c
//Stack.h
#include <iostream>
#include <assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
//只能在这里给缺省值
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;
}
6.函数重载
C++支持在同一作用域中出现同名函数,但是这要求函数的形参不同,可以是形参的个数不同,也可以是形参的类型不同:
c
#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;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
返回值不同不能作为重载条件,因为调用时也无法区分
c
//返回值不同不能作为重载条件,因为调⽤时也⽆法区分
void fxx()
{}
int fxx()
{
return 0;
}
注意点:
c
//下⾯两个函数构成重载
//但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
//err,对重载函数的调用不明确
f1();
return 0;
}
7.引用
7.1什么是引用
引用其实就是给变量起别名,引用并不是新定义了一个变量,编译器不会给引用变量开辟空间
7.2引用的定义
cpp
//引用的定义
类型& + 引用别名 = 引用对象
引用的使用如下:
cpp
//类型 & +引用别名 = 引用对象
int main()
{
//定义一个整形变量
int a = 10;
//为整形变量起别名
int& ra = a;
//为别名起别名
int& rra = ra;
//为同一个整形变量起多个别名
int& rb = a;
//它们指向的地址相同,说明并没有为它们单独开辟空间
cout << &a << endl;
cout << &ra << endl;
cout << &rra << endl;
cout << &rb << endl;
return 0;
}
而引用的原理为:
对变量赋值和对变量引用的区别:
7.3引用的使用
因为引用对象和原对象指向的空间相同,所以说,对引用对象进行修改,原对象也会被修改,引用的底层实现其实是指针,所以我们可以这样写代码:
c
//引用的使用
//对引用对象的修改会直接影响到原来的对象
//所以这里交换就会成功
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int x = 10;
int y = 20;
//10 20
cout << x << " " << y << endl;
Swap(x, y);
//20 10
cout << x << " " << y << endl;
return 0;
}
Swap函数之前使用指针也能够完成,那么有了指针,引用是不是多余了呢?肯定不是,我们看下边的一个例子:
cpp
//假设此时我们已经创建了一个栈
//void STInit(ST& rs, int n = 4);栈的初始化
//void STPush(ST& rs, STDataType x);入栈
//int& STTop(ST& rs);//获取栈顶元素
int main()
{
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
//此时我们要获取栈顶元素:2
cout << STTop(st1) << endl;
return 0;
}
获取栈顶元素比较简单,但是这时我们想要对栈顶元素进行修改,方法如下:
c
// int STTop(ST& rs)
//如果要对栈顶元素进行修改,只需要将返回类型改为引用即可
//此时返回的是对原始对象的引用,而不是对象的副本(如果返回值的话,返回时需要将值先存储到一个临时空间中,所以是对象的副本)
int& STTop(ST& rs)
{
return rs.a[rs.top-1];
}
//假设此时我们已经创建了一个栈
//void STInit(ST& rs, int n = 4);栈的初始化
//void STPush(ST& rs, STDataType x);入栈
//int& STTop(ST& rs);//获取栈顶元素
int main()
{
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
//此时我们要获取栈顶元素:2
cout << STTop(st1) << endl;
//此时我们要读栈顶元素进行修改:
STTop(st1)++;
cout << STTop(st1) << endl;//输出3
return 0;
}
这时如果要用指针来实现的话是非常麻烦的,感兴趣的可以实现一下!
7.4引用注意事项
1.由于引用作为返回值或传参不需要开辟临时变量,所以可以提效,而且还可以通过引用直接修改到原变量,所以能够用引用的地方使用引用比指针更加方便
2.引用和指针在实践中是相辅相成的,功能有重叠性,各有特点,互相不可替代,但是与Java中的引用不同,C++中的引用不能够改变指向
3.引用可以简化程序,比如使用引用代替指针传参,可以避免复杂的指针,更容易理解
4.引用必须初始化
5.引用一旦引用一个实体,就不能引用其他实体
6.引用时如果引用临时变量,就可能出现野引用的情况
c
int main()
{
int a = 10;
int b = 20;
//5.引用一旦引用一个实体,就不能引用其他实体:
int& ra = a;
//&ra = b; err:无法从int转换成int*
ra = b;//这里是赋值,此时a = ra = b = 20
//4.引用必须初始化
//int& rb; err:必须初始化引用
return 0;
}
cpp
//野引用
int& f()
{
int a = 10;
return a;
}
7.5const引用
总的来说,说的其实就是权力的问题,权力可以缩小,但是不能扩大
7.5.1示例1--权力扩大问题
cpp
//const引用示例1
int main()
{
const int a = 10;//此时a为const类型
//int& ra = a;//err,权限不能扩大,不能从const int转换为int&
const int& ra = a;//√,const int&和const int权限相同
return 0;
}
7.5.2示例2--权力缩小
cpp
//const引用示例2
int main()
{
int a = 10;
const int& ra = a;//权力可以缩小
int& rb = a;//正确
int& rra = ra;//err,无法从const转换成int&
return 0;
}
7.5.3示例3
cpp
int main()
{
int a = 10;
int& rb = 30;//err,因为30是常数
const int& rrb = 30;//这个才是正确的写法
int& ra = a*3;//err,因为a*3是常数
const int& rra = a*3;//这样也是正确的,表示a*3被rra引用,可以通过rra找到a*3,但是不能改变a*3的值
rra = 20;//err,不能给常量赋值
double c = 1.2;
const int& rc = c;//虽然是int类型,但是这样也是对的,涉及到整形提升
//这里就是先将1.2传到一个int类型的临时变量中,然后再让rc进行指向
return 0;
}
7.6指针与引用之间的关系
C++中的指针和引用就像两个性格不同的亲兄弟,指针是哥哥,引用为弟弟,它们在实践中相辅相成,但各自有各自的特点,互相不可替代
1.语法概念上,引用是为变量起别名,不需要开辟空间,指针存储的是一个变量的地址,需要开辟空间
2.sizeof引用的大小是引用对象的类型发=大小,而sizeof指针始终是4/8个字节
3.引用在定义时必须初始化,指针只是建议初始化
4.引用在引用一个对象后,就不能引用其它对象,而指针可以不停地改变指向
5.引用可以直接访问引用对象,而指针需要解引用
6.指针很容易出现野指针情况,而引用比较难以出现野引用的情乱
8.inline
8.1define定义宏的劣势
之前我们就已经讲过,define定义的宏具有很大的劣势,比如我们要定义一个宏函数:
c
//错误写法1:
#define ADD(int x,, int y) return x+y
//错误写法2:
#define ADD(int x, int y) (x+y)
//错误写法3:
#define ADD(x, y) (x+y)
//正确写法:
#define ADD(x, y) ((x) + (y))
//1.形参中不用追加类型
//2.必须加上合适的小括号
//3.最后边不能加;因为是宏替换
//4.外面不加括号:x=1+2, y=2+3形成错误
//5.里边不加括号:x=1|2,y=2^3形成错误,位移运算符优先级低于+
为了解决这一个问题,C++中设计了一个inline函数:
8.2inline函数的使用
cpp
//inline函数的使用
//直接在函数前加上关键词inline即可
inline int ADD(int x, int y)
{
return x + y;
}
int main()
{
cout << ADD(10, 20) << endl;//输出30
return 0;
}
8.3inline函数使用注意事项
总结:
1.inline修饰的函数被称为内联函数,在使用内联函数时,不需要为改函数创建栈帧,可以提高效率
2.inline对于编译器来说只是建议,也就是说,内敛函数的展开与否取决于编译器,一般内联函数短小时就会展开,内联函数较大时就不会展开
3.inline不建议声明和定义分离到两个文件,分离会导致链接错误,因为inline被展开,是没有函数的地址的,链接时就会报错
4.VS中debug版本下是默认不展开的,想要展开可以搜索着改动一下编译器
9.nullptr
NULL其实是一个宏,在传统的C头文件中,可以看到:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
也就是说,C++中,NULL可能被定义为字面量0,或者在C中被定义为(void*)的常量,这样在使用重载的函数时,就会遇到问题:
cpp
//两个重载函数:
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL); //这里调用的也是f(int x)函数,因为NULL被解析成了0
f((int*)NULL); //编译报错:error C2665: "f": 2 个重载中没有⼀个可以转换所有参数类型f((void*)NULL);
return 0;
}
既然有着这样的问题,那么就设计出一个新的东西:nullptr
nullptr是一个特殊的关键字,是一种特殊类型的字面量,它可以转换成其它任意类型的指针类型,nullptr只能被转换成指针类型,而不能被转换成整数类型,所以以后用nullptr千万不要陌生!