本篇C++入门主要是讲C++对C语言不足的一些弥补,把这部分学好,为我们以后C++进阶打好坚实的基础。
C++总计有63个关键字,其中C语言的就占了32个。
所谓关键字就是我们用来控制语法的关键标识符,比如类型、循环、判断等等
我们先来看一个C里面的常见问题:
现在我想打印rand这个变量的值
#include
int rand = 0;
int main()
{
printf("%d",rand);
return 0;
}
但如果我们再加一个头文件#include
#include
#include
int rand = 0;
int main()
{
printf("%d",rand);
return 0;
}
发现这里居然报错了
这里的报错原因是因为我们的stdlib这个头文件里面也有rand命名的函数,与我们这里命名的全局变量rand产生了冲突。
这就是C语言的一大缺陷,在我们的项目中,难免会有相同名字的变量、函数等,但是一旦命名相同,C语言就无法编译通过
所以,C++旨在改进这一缺陷,引入了“命名空间”这一概念
具体做法就是在指定的名称前加一个namespace,有点类似我们C语言学习的结构体
namespace test{//这里的命名空间叫test,你也可以取其他名字
int rand = 0;
}//与结构体不一样的是,C++的命名空间不用加分号
int main()
{
printf("%d",rand);
return 0;
}
那命名空间到底是什么东西?
我们大白话说就是,现在我家住在山上,我家里养了几头猪,然后山里也有一些野猪
那我们为了区别到底是野猪还是家猪,那我们就装一个围栏,把我们的家猪给围起来。
这个围栏就是namespace
但是细心的同学会发现,这里还是有一个警告,这个警告其实就是告诉我们%d需要的是int型,但我们给的是指针类型,如果需要打印指针类型就是%p,
如果就是要打印rand的值,那我们就需要用到“围栏的钥匙”,也就是域作用限定符::
int main()
{
printf("%p\n", rand);//%p打印的是地址
printf("%d", test::rand);//如果要找到家猪rand,需要找到对应家猪的围栏test
//这里的::就是域作用限定符,相当于围栏的钥匙
return 0;
}
所以,有了命名空间,我们就可以解决命名冲突的问题,如果不同程序员定义了多个同名变量,那就设多个命名空间,通过域作用限定符来使用不同的同名变量
int main()
{
printf("%d\n", test::rand);//如果要找到家猪,需要找到对应家猪的围栏
//这里的::就是域作用限定符
printf("%d\n", test1::rand);
printf("%d\n", test2::rand);
return 0;
}
命名空间除了定义变量,也可以定义函数、结构体等
namespace test{//命名空间test
int rand = 0;//可以定义变量
int add(int x, int y)//可以定义函数
{
return x + y;
}
struct LNode {//可以定义结构体
struct LNode* next;
int val;
};
}
命名空间就是建了一个围栏,你要找猪,默认不会去围栏里去找。如果想要找围栏里的猪,请给出“围栏名称和围栏钥匙”(也就是命名空间的名字和作用域限定符::)
比如这里我想调用add函数,但是没有给命名空间和作用域限定符,编译器就会报错
我们把命名空间加上,代码就可以正确运行了
namespace test{//命名空间test
int rand = 0;//可以定义变量
int add(int x, int y)//可以定义函数
{
return x + y;
}
struct LNode {//可以定义结构体
struct LNode* next;
int val;
};
}
int main()
{
int x=test::add(1, 2);
printf("%d", x);
return 0;
}
命名空间的结构体也是类似的,比如
struct test::LNode node;
相当于是struct LNode node,
只不过这里中间的LNode是定义在test里,所以我们把LNode换成test::LNode
namespace test{//命名空间test
int rand = 0;//可以定义变量
int add(int x, int y)//可以定义函数
{
return x + y;
}
struct LNode {//可以定义结构体
struct LNode* next;
int val;
};
}
int main()
{
struct test::LNode node;
//相当于是struct LNode node,
//只不过这里中间的LNode是定义在test里,所以我们把LNode换成test::LNode
return 0;
}
可能会有老铁问:“那如果我命名空间里的变量同名产生冲突怎么办?”
答:那你就在命名空间里再定义一个命名空间呗,每有一层命名空间,就多用一个作用域限定符::
namespace test{//命名空间test
int rand = 0;//可以定义变量
namespace test1 {
int rand = 1;
}
}
int main()
{
int x = test::rand;
int y = test::test1::rand;
printf("x=%d\n", x);
printf("y=%d\n", y);
return 0;
}
但是这样做的话还有一个问题,你每次想要用自己定义的,在命名空间那个变量/函数/结构体,都要加上命名空间和作用域限定符::,这你不觉得烦吗?
所以我们这里可以用
using namespace 命名空间名称;
这样就默认了到某个命名空间里去用它的变量/函数/结构体
使用了这种,你可以直接访问命名空间的,也可以用::来访问
#include
namespace test{//命名空间test
int rand = 0;//可以定义变量
}
using namespace test;
int main()
{
int x = test::rand;
int y = rand;
printf("x=%d\n", x);
printf("y=%d\n", y);
return 0;
}
这就有了我们经常用的
using namespace std;
std是我们c++官方定义的命名空间
ps:在工程项目中不要轻易展开std库,因为很容易你自己写的和库中的冲突。日常自己练习一般无所谓。
到这里,大家可能会发现,我们前面好像打印输出函数还是我们c语言的那一套,那我们c++有没有自己的呢?有!
#include
using namespace std;
int main()
{
cout << "hello world" ;//“<<”是流插入运算符,cout是std库里的一个
//c是console控制台的意思,out是出来的意思
//这里涉及到c++的对象知识,我们先学怎么用,具体的原理后面学习面向对象我会详细介绍
//你可以理解为"hello world"流向了cout控制台
//cout很牛逼在于,它可以自动识别类型
int a = 1;
float b = 3.14;
char c = 'x';
cout << a<<"\n";//如果要换行,就在后面跟一个<<"\n",表示\n也流向了cout控制台
cout << b<<endl;//换行法二:在后面加一个<
//endl表示end 这个line,就是结束这一行的意思
cout << c<<endl;
//你也可以一次性全部流入,比如
cout << a << b << c;
return 0;
}
ps:我们直接using namespace std其实是风险很大的,因为你一个人直接展开库里面的全部,就会导致别人很可能定义的变量和命名空间里的产生冲突,所以我们这里建议只展开std里面的cout和endl
using std::cout;
using std::endl;
另外还有一点,如果你想控制打印出来的精度,比如我想打印一个浮点数,只到小数点后1位,c++是不太方便的,这里你还是用c语言的printf来控制(c语言能用的c++也可以用)
int main()
{
float x = 3.1415926;
printf("%.2f", x);
}
这里的输入也就对应了C语言的scanf
我们c++里面就是cin
c是console控制台的意思,in是进去的意思
需要注意的是,因为我们这里是输入嘛,所以用cin>>,这里的符号和输出也是反过来的
using std::cout;
using std::endl;
using std::cin;
int main()
{
int a = 0;
cout <<"请输入a值:";
cin >> a;
cout << "a值为"<<a;
//cout叫流插入
//cin叫流提取,它同样可以自动识别类型
}
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
所谓全缺省,就是你定义的函数,这个函数的参数你都预先赋了值
using namespace std;
void func(int a = 1,int b=2, int c=3) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main() {
func();//缺省a和b和c,默认a=1,b=2,c=3
func(10);//缺省b和c,默认b=2,c=3
func(10,20);//缺省c,默认c=3
func(10,20,30);
}
这种缺省方式就是有一部分参数预先赋了值
注意:
如果采用了半缺省,你就不能一个参不传了。
因为半缺省肯定是有参数没有提前定义的,如果你一个参数不传,那那个没有提前定义的参数就没有值给它了,就会报错。
using namespace std;
void func(int a ,int b=2, int c=3) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main() {
func(10);//10传参给a;缺省b和c,默认b=2,c=3
func(10,20);//10和20传参给a和b;缺省c,默认c=3
func(10,20,30);//10,20,30传参给a,b,c
}
函数重载有点像我们中文的“一词多义”
说白了就是函数名相同,参数类型不一样、个数不一样、参数顺序不一样,返回值可以不一样,可以一样
ps:C语言是不支持同名函数的,但是我们c++可以支持,只是有一些限制
using namespace std;
int add(int a, int b) {
cout << "int add(int a,int b)" << endl;
return a + b;
}
double add(double a, double b) {
cout << "double add(double a, double b)" << endl;
return a + b;;
}
int main()
{
int x=add(1, 2);
cout << x << endl;
double y=add(1.1, 2.2);
cout << y << endl;
return 0;
}
小细节:如果你有两个不同的重载,这时你传的参数和这两个重载都不一样,比如
这里就会有歧义了,因为有两个重载,你到底是把int类型的1转成double还是把double类型的2.2转成int类型呢?所以这里会报错
但是如果你只有一个函数,那你这样传就没有毛病了,编译器会自动给你进行类型转换
(因为现在只有一个函数了,只要把传参过去类型不对的转换一下就行,没有重载函数的歧义)
这是对C语言改进最大的地方,C++里面就没有指针的概念了(当年C++的祖师爷也觉得指针过于复杂)
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
using namespace std;
int main()
{
int a = 1;
int& b = a;
int& c = b;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
比如现在有a、b、c三个变量
b是把a的值赋过去,b和a不是一个变量,它们独占不同空间
但是我们这里int& c=a,这里就不一样了,c其实就是a的别名,
c和a其实是一个东西,它们占用同一份空间
如果你这里b++,那么a不会受到任何影响
但如果你对c–,不好意思,a就是c,c就是a,即a–
ps:引用必须初始化,如果你int& b,不给b初始化,谁知道这个b是哪个变量的别名,所以c++的引用必须初始化。
c++的别名一旦确定,中途是不能改变对象的(比如a起了别名b,那b就一直是a的别名)
int main()
{
int a = 0;
int& b = a;
int c = 1;
b = c;//这里是把c值赋给b,还是让b称为c的别名?
cout << b << endl;//打印1
cout << &a << endl;//a和b同地址,a和c不同地址
cout << &b << endl;
cout << &c << endl;
//说明b=c是赋值,是把c的值赋给b(b是a的别名,也就是把c的值赋给a)
//但是b还是a的引用
}
我们c语言经常写一个交换函数swap
int swap(int* x,int* y){
int tmp=*x;
*x=*y;
*y=tmp;
}
那我们c++就不需要这么麻烦了,我们之前说过,引用就是取别名,引用就是它自己,它自己就是引用。
所以我们c++这里swap函数就很容易写了
void swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int x = 1;
int y = 2;
swap(x, y);
cout<<"x=" << x << endl;
cout<<"y=" << y << endl;
return 0;
}
如果不是引用作为返回值,比如下面代码
我们很容易知道x的是1
int count()
{
int n = 0;
n++;
return n;
}
int main()
{
int x = count();
cout << x << endl;
return 0;
}
这段代码,我们先是在main函数里创立了ret变量,然后调用Count函数
创立Count函数栈帧,Count里又会创建变量n
这里Count函数调用完,栈帧就销毁了,所以我们返回的其实是n的复制,返回值为1。
(因为你栈帧已经销毁了,n也肯定销毁了,你再返回n的位置不就是野指针吗?)
但如果我们用的是引用返回
我们知道引用是我们某个变量的别名,引用就是那个变量
那你返回引用就是返回那个变量
可以看到,如果我们用引用做返回值,这里是报了一个警告的
因为你count函数结束,其实栈帧已经销毁了,
如果你还要访问原先函数里的变量,其实是非法访问。
至于这里返回值和前面的一样都是1,只是因为vs这个编译器是1,如果换别的编译器结果就不一定
如果用引用做返回值,返回值是不确定的
因为你用引用做返回值,返回值到底是多少是取决于函数栈帧销毁后,到底有没有把原先的空间清理掉,如果清理掉,那就不一定是原来的值了。我们这里vs编译器函数栈帧销毁后是没有清理原先空间,至于其他编译器就未可知了。
比如下面这个示例,大家就会对我上面说的话有更深的理解
int& count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& x = count();
cout << x << endl;
cout << x << endl;
count();
cout << x << endl;
return 0;
}
我们这里用引用x来接收,count函数返回的引用,这就意味着x和n是一个位置的
第一次我们打印了1,那是因为count函数栈帧里的东西还没销毁
第二次打印了一个1462811616,这个数就是count栈帧里东西销毁了,这个数可能是其他程序当时正在用那个位置生成的数。
第三次,由于我们又调用了count,导致n那个位置又生成了1,所以我们第三次打印出来是1
再举一个例子
int& add(int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int& x = add(1,2);
cout << x << endl;
add(3,4);
cout << x << endl;
return 0;
}
这里我们用x来接收add函数的引用
第一次我们x=add(1,2),并且由于编译器原因,函数结束栈帧销毁,原先变量位置并没有清除,所以打印了3
但第二次我们没有对x做任何改变,只是中途调用了add(3,4)
为什么打印x是7?
因为x接收的是add函数返回的c的引用,x和c其实是一个位置的同一变量
x其实就是c,所以第二次c改变了,x也是跟着改变,x=7
上面的3和7都是建立在vs这个编译器在函数栈帧销毁后没有清除原先变量空间的前提下,如果是不同编译器,打印情况应该如下:
这里为什么第二次调用add,x和c还是同一个位置的同一变量?
因为你再一次创建函数栈帧,函数栈帧还是那个函数栈帧,函数里的变量位置也还是不变的,
如下代码所示:
void func()
{
int c = 0;
cout << &c << endl;
}
int main()
{
func();
func();
return 0;
}
void func1()
{
int c = 0;
cout << &c << endl;
}
void func2()
{
int a = 0;
cout << &a << endl;
}
int main()
{
func1();
func2();
return 0;
}
并不是说同一个函数才能用同一个空间,不同函数也是用的一个同一块空间
只不过说不同函数它可能里面变量个数不同,它占用同一块空间的大小不同。
可以发现,我们虽然上面这样写,但是其实这个代码是有逻辑上的危险的
那什么时候可以用引用返回是安全的呢?你要么用静态的变量,或者栈上的,你malloc的。简而言之就是出了作用域,那个变量不会销毁,那你用引用就是安全的。
对于静态变量,我们来看一个例子:
int& add(int a, int b) {
static int c = a + b;
return c;
}
int main()
{
int& x=add(1,2);
cout << x << endl;
add(3, 4);
cout << x << endl;
return 0;
}
这里x是c的别名,c又是一个局部静态变量,局部静态变量只会被初始化一次
第一次调用add(1,2),我们初始化了局部静态变量c为3
第二次调用add(3,4),由于局部静态变量c已经被初始化过了,
所以第二次是不执行static int c = a + b;的
所以我们的x也就是c的别名,还是3
再来看另外一种情况:
int& add(int a, int b) {
static int c = 0;
c = a + b;
return c;
}
int main()
{
int& x=add(1,2);
cout << x << endl;
add(3, 4);
cout << x << endl;
return 0;
}
如果这样写,我们局部静态变量只会被初始化一次,但是c=a+b;是每次都要调用的
所以我们第一次打印了3,第二次打印了7。
看似两个情况是相同的代码,其实内在是完全不同的。
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
为什么能提高效率,大白话就是,如果你传值返回,如果那个值是个特别大的结构体,那返回消耗的代价肯定也很大。但如果你传引用返回,其实就和返回一个指针差不多,占用空间会小很多的。
如果一个变量已经被const修饰了,也就说它已经不能被修改了
如果这时你还对常变量取别名是有问题的。
因为我常变量a本身已经不能修改了,但是你给我取个别名b还能修改,这就产生矛盾了。
所以如果想给常变量取别名,别名也必须是const修饰的,如下图,这样就不会报错了
另外,虽然说const修饰的变量必须用const修饰的别名
但是,没有const修饰的变量也可以用const修饰的别名
(有点类似于权限不能放大,但是权限可以缩小)
还有一个需要提醒的点,这个大家应该不说也清楚。
就是不同类型的引用也是不可以互相用的
比如这里j是doible类型的,你可以把i强转,赋值给j
但是rj是double类型的引用,它只能是double类型变量的别名,你int类型的i就不可以用rj这个别名
进一步的,如果我们在double&前加const,这里又不报错了。
这是因为我们在强转的时候,会默认生成一个临时变量,通过把int类型的先变成double类型的临时变量,再把临时变量赋给double类型的变量。
而生成的临时变量又具有常变量的性质,所以我们const double& rj=i是可以的。相当于先产生一个const double类型的临时变量,再把这个临时变量赋给rj。
语法上,我们c++的引用就是原来的变量,它们共享一块空间
但是底层实现其实引用和指针是一样,其实是开辟了额外空间的
int main()
{
int a = 10;
int& b = a;//语法上b没有开额外空间,b就是a,a就是b
//但是底层实现其实开了空间
return 0;
}
所以,引用底层实现就是指针,但是我们语法上默认c++的引用就是和原先变量共享一块空间。
举个例子
int main()
{
char ch = 'x';
char& c = ch;
cout << sizeof(c) << endl;
return 0;
}
按道理这里c其实底层是个指针,但是我们语法上还是默认引用就是原先变量
所以这里sizeof(c)其实就是sizeof(ch),也就是1
引用和指针的不同点:
引用概念上定义一个变量的别名,指针存储一个变量地址。
引用在定义时必须初始化,指针没有要求
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
没有NULL引用,但有NULL指针
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
有多级指针,但是没有多级引用
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
引用比指针使用起来相对更安全
ps:引用不是百分百安全的,比如前面讲的,你对出了某个作用域就销毁的变量进行引用,就会产生问题。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调
用建立栈帧的开销,内联函数提升程序运行的效率
你就简单记住,写函数的时候,在返回值类型前加一个inline,就可以提升效率。其他的和正常写函数没啥区别
inline int add(int x, int y) {
int z = x + y;
return z;
}
int main()
{
int x = add(1, 2);
cout << x << endl;
return 0;
}
ps:内联更适合小函数,如果是那种代码比较多的大函数或者递归的函数,用内联就不合适。就算你大函数加内联,编译器也会默认大函数没有加内联。
auto可以自动推倒类型
int main()
{
int a = 0;
int b = a;
auto c = a;//auto可以自动推倒类型,这里把int型的a赋给c,那么就默认c是int型
auto d = &a;//指针可以显示的写,也可以不显示的写
auto* e = &a;
auto& f = a;//引用必须显示的写
f++;
cout << typeid(c).name() << endl;//typeid(变量名).name()可以用于打印一个变量的类型
cout << typeid(d).name() << endl;//d是int型a的指针,所以打印int *
cout << typeid(e).name() << endl;
//指针可以显示的写,也可以不显示的写,所以这里auto d=&a和auto*e=&a,d和e的类型是一样的
cout << typeid(f).name() << endl;
//f是a的引用,f和a的类型一样都是int
return 0;
}
但是上面的代码,说实话没有体现出auto真正的意义,它真正的作业需要到vector顺序表那块知识才能体现出来。比如下面这段代码:定义对象时,类型较长,用auto比较方便,但是auto不能作参数,也不能做返回值(有些比较新版本支持做返回值,但是强烈不建议做返回值,你返回一个值,都不知道是什么类型,我怎么知道用什么接收?)
auto可以让一个很长的定义变得很短哈哈哈哈,这段代码如果暂时看不懂没关系,后面会详细讲
另外,auto是不能用来声明数组的,别问为什么,当时写c++这么语言的创始人是这么规定的。
在C++98中如果要遍历一个数组,可以按照以下方式进行:
int main()
{
int arr[] = { 1,2,3,4,5 };
//对数组的三种遍历方法
//法一
for (int i = 0;i < sizeof(arr) / sizeof(arr[0]);i++) {
arr[i] *= 2;
}
//法二
for (int* p = arr;p < arr + sizeof(arr) / sizeof(arr[0]);p++) {
cout << *p<<" ";
}
cout << endl;
//法三
for (auto e : arr) {//会依次取数组arr中的数据赋给e对象,自动判断结束,自动++循环往后走
cout << e <<" ";
}
}
需要注意的是,法三这个是对赋值后的e进行打印,并没有对原先的数进行更改,比如:
int main()
{
int arr[] = { 1,2,3,4,5 };
for (auto e : arr) {
e++;
cout << e <<" ";
}
cout << endl;
for (auto e : arr) {
cout << e << " ";
}
}
当然了,如果你是用引用来接收数组数据,那么数组中的元素是会跟者变化的
int main()
{
int arr[] = { 1,2,3,4,5 };
for (auto& e : arr) {
e++;
cout << e <<" ";
}
cout << endl;
for (auto e : arr) {
cout << e << " ";
}
}
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
void func(int) {
cout << "f(int)" << endl;
}
void func(int*) {
cout << "f(int*)" << endl;
}
int main()
{
int* ptr = NULL;
func(0);//调用int那个函数
func(NULL);//这里依旧是调用了int那个函数
//NULL实际是个宏,在传统c语言头文件stddef.h中,NULL本质就是0
//这个是当时写这个库的时候,那个创作者定义的
func(ptr);//调用int*那个函数
func(nullptr);//调用int*那个函数
//我们一般初始化空指针也是用nullptr
return 0;
}