硬核两万字文章带你C++入门

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核两万字文章带你C++入门相关的知识,希望对你有一定的参考价值。

C++入门

C++关键字

C语言关健字32个,C++关键字63个关键字

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

C++是在C的基础上发展起来的,C++是兼容C的大多数的语法

命名空间

C++是怎么输出hello world的呢?和C语言有什么区别呢?

#include<iostream>
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}

我们创建一个C++文件后,写出上面代码就会在屏幕上打印hello world,那么上面代码中的using namespace std;是什么意思呢?这就是我们要讲的命名空间。

C++增加命名空间是为了解决C语言的不足,在一段C语言代码中,我们编译下面的代码会报错:

#include<stdio.h>
#include<stdlib.h>
int rand =10;
int max=10;
int main()
{
    printf("%d\\n",rand);
    return 0;
}

那么为什么会报错呢?是因为我们包含了stdlib.h这个头文件

这是一个命名重定义的问题,在我们编写C语言代码时,我们定义变量名时,可能与库函数中的命名冲突了,实际大型项目开发,还存在同事之间定义的变量、函数、类型命名冲突等等

所以为了弥补C语言的不足,C++提出了命名空间来解决命名冲突的问题

那么命名空间是怎么定义的呢?

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名
空间的成员。注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

#include<stdio.h>
#include<stdlib.h>
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域,名字叫Z
{
    //可以定义变量、函数、类型
    int rand = 10;
}
int main()
{
    printf("%d\\n",Z::rand);//::域作用限定符
    return 0;
}

::是域作用限定符,通过这个我们可以找到命名空间中的定义的变量或者其他东西,这样我们就可以定义rand,并可以成功的打印它

我们也可以在里面定义函数:

#include<stdio.h>
#include<stdlib.h>
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域,名字叫Z
{
    //可以定义变量、函数、类型
    int Add(int left,int right)
    {
        return left+right;
    }
}
int main()
{
    Add(1,2);//直接调用他找不到,因为他只会在全局里面找,不会去域里面
    Z::Add(1,2);
    return 0;
}  

这里需要注意的是直接调用他找不到,因为他只会在全局里面找,不会去域里面,所以我们需要利用域作用限定符去调用Add函数

命名空间还可以嵌套定义:

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
    namespace S//会将外面的定义的变量隔离开来,定义了一个命名空间域
    {
        int Sub(int left,int right)
        {
            return left-right;
        }
    }
}
int main()
{
    Add(1,2);//直接调用他找不到,因为他只会在全局里面找,不会去域里面
    Z::Add(1,2);
    Z::S::Sub(1,2);
    return 0;
}  

我们在调用嵌套的命名空间的函数时,比如我们现在调用Sub函数,我们需要用Z::S::Sub(1,2);去调用该函数

在一项工程当中不同文件的命名空间的定义名字可以相同,并且该命名空间中的定义编译器会合并在一起,比如:

在Add.cpp这个文件中我们定义了一个命名空间

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
}

在Add.h中我们定义了命名空间

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int Add(int left,int right)
    {
        return left+right;
    }
}

编译器会将这两个文件中定义的空间进行合并:

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
}

那么我们如何使用命名空间的东西呢?

有三种方式:

  • 1、全部直接展开到全局
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
}
using namespace Z;//将Z命名空间展开
int main()
{
    Add(1,2);
    return 0;
}

优点是用起来方便。缺点是把自己的定义暴露出去了,导致命名污染

using namespace std;//std是包含C++标准库的命名空间

std是包含C++标准库的命名空间,这里其实就将C++标准库展开了,这就解释了我们开头打印hello world时,前面为什么有一个using namespace std;这里就很好的解释了这个代码的意思

  • 2、访问每个命名空间中的东西时,指定命名空间
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
}
int main()
{
    Z::Add(1,2);
    return 0;
}

优点:不存在命名污染。缺点:用起来麻烦,每个都得去指定命名空间

  • 3、将命名空间中常用的展开
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域,名字叫Z
{
    //可以定义变量、函数、类型
    int rand = 10;
    int Add(int left, int right)
    {
        return left + right;
    }
}
int main()
{
    using Z::Add;
    return 0;
}

不会造成大面积的污染,也可以解决每个都指定命名空间的问题,这是一个将1和2折中的解决方案


C++输入&输出

这又回到了我们开始提到的C++是怎么输出hello world到屏幕上的,我们来看下C++是如何来实现的:

#include<iostream>
//展开常用的
using std::cout;
using std::endl;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}

按照上面我们所讲的,std是一个命名空间,命名空间中有我们要使用的cout和endl,所以我们这里只将常用的展开,但是在日常学习中,我们并不需要像项目中那么规范,我们可以直接将std命名空间展开。

#include<iostream>
//展开常用的
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}

我们了解了输出,那么C++是如何进行输入的呢?我们使用cin标准输入,看下面代码:

#include<iostream>
//展开常用的
using namespace std;
int main()
{
    int n;
    cin >> n;//>>输入运算符 流提取运算符
    int* n = (int*)malloc(sizeof(int)*n);
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    for(int i=0;i<n;i++)
    {
    	cout << a[i]<<" ";//<<输出运算符/流插入运算符  可以连续输出
    }
    cout<<endl;
    //等价于
    cout<<"\\n";
}

注意:使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空
间。早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。

我们在输出时可以连续输出,cout<<endl;实际上就等价于cout<<"\\n"。

另外C语言输入输出时需要指定类型的,C++不用指定类型可以自动识别类型:

double* n = (double*)malloc(sizeof(double)*n);
for(int i=0;i<n;i++)
{
    cin>>a[i];//自动识别类型
}
int main()
{
    int i=2;
    double d = 1.111;
    int* pi = &i;
    cout<<i<<endl
    cout<<d<<endl
    cout<<pi<<endl;
    return 0;
}

在写C语言printf和scanf函数时,前面我们需要指定输入输出的格式,而C++则是自动识别类型。

而在下面的场景中,用printf会更好一点

struct Student
{
	char name[10];
	int age;
};
int main()
{
	struct Student s = { "张三", 18 };
	cout << "名字:" << s.name << " " << "年龄:" << s.age << endl;
	printf("名字:%s 年龄:%d\\n", s.name, s.age);
	return 0;
}

缺省参数

缺省参数概念

所谓缺省参数,顾名思义,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就不需要传参。可以传参数,也可以不传,如果不传,函数参数用缺省的

我们看下面代码:

#include<iostream>
using namespace std;
void TestFunc(int a = 0)//参数缺省值
{
    cout<<a<<endl;
}
int main()
{
    TestFunc();//不传会用默认值
    TestFunc(10);//传了就缺省参数就没用了
    return 0;
}

我们可以看到没有传参的使用了缺省值,而传参的就忽略缺省值了,只考虑传过来的参数

那么缺省参数有什么用呢?

我们在数据结构中,设计栈这个数据结构时:

void StackInit(struct Stack* ps, int defaultCP)
// 假设我明确知道这里至少要存100个数据到st1里面去
struct Stack st1; 
StackInit(&st1, 100);

如果我们假设明确知道一个栈st1最少使用100个空间,我们给栈的容量初始化为100,所以这时我们在栈初始化这个接口引入一个参数defaultCP,我们想要弄100个容量,我们直接传参100就可以了。

但是当我们不能明确知道栈st2的需要使用的空间,如果上面不设置缺省参数,那么我们必须要传一个值进去,那我们传的值大了的话,会造成空间浪费,传的小了的话会不够用,那么此时这个缺省参数就派上用场了,我们这样定义:

void StackInit(struct Stack* ps, int defaultCP = 4)
struct Stack st2; 
StackInit(&st2);

不传值时默认它为4,不够了再进行扩容就可以了


缺省参数分类

全缺省参数

void TestFunc(int a = 10, int b = 20, int c = 30)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}

全缺省参数顾名思义就是全部都有缺省参数,我们在传参时不能间隔着传,比如:

TestFunc(100, ,50);//这样是错误的

编译器是不允许这样进行传参的

半缺省参数

void TestFunc(int a, int b = 10, int c = 20)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
int main()
{
    //没有缺省的必须传参
    //半缺省参数必须从右往左依次来给出,不能间隔着给
    TestFunc(1);
    TestFunc(1,2);
    TestFunc(1,2,3);
    
    return 0;
}

注意:

  • 没有缺省的必须传参

比如这上面的a参数必须要传参

  • 半缺省参数必须从右往左依次来给出,不能间隔着给

比如:

void TestFunc(int a = 30, int b = 10, int c)

这样就是错误的,没有按照从右往左的顺序

也不能这样:

void TestFunc(int a = 30, int b, int c = 10)
  • 缺省参数不能在函数声明和定义中同时出现
//a.h
void TestFunc(int a = 10);
//a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

  • 缺省值必须是常量或者全局变量
void TestFunc(int a = x);//error
  • C语言不支持(编译器不支持)

C语言是不支持缺省参数的。


函数重载

函数重载概念

重载函数是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者类型顺序)必须不同,也就是说用同一个函数完成不同的功能。

//函数重载
int Add(int left, int right)
{
	return left+right;
}
double Add(double left, double right)
{
	return left+right;
}
long Add(long left, long right)
{
	return left+right;
}
int main()
{
    Add(10,20);//会调用对应参数类型的函数
    Add(10.0,20.0);
    Add(10L,20L);
    
    return 0;
}

函数重载面试:为什么C++支持函数重载,而C语言不支持?

C编译器,直接用函数名关联,当函数名相同时,它无法区分

C++如何支持重载呢?

函数名修饰规则:不能直接用函数名对函数进行修饰,代入参数特点修饰,函数名相同,只要参数不同,修饰出来的名字就不同,就可以区分两个函数了,就支持重载了,把定义在文件中的函数调用,地址找到(符号表里找),链接在一起,所有.o文件进行合并,生成一个执行文件。

函数重载:

要求参数不同,因为参数不同修饰出来的名字就不同

编译器能不能实现函数名相同参数相同返回值不同,就能构成重载?

不行

int func();//-> _Z4ifunc
double func();//-> _Z4dfunc

如果把返回值带进修饰规则,那么编译器层面是可以区分的。

但是语法调用层面,无法区分,带有严重的歧义!

func();调用时,到底是调用哪个呢?这是不知道的,所以不能


名字修饰

extern"C"

C++实现编写成动态库或者静态库,写一个C++的程序去调用这个库是没问题的,但是我们写一个C程序就不行,程序是用因为链接时会有问题,比如静态库有一个函数void* tcmalloc(size_t n) (谷歌提供的更高效替代malloc的库)

C程序在链接时,直接用函数名tcmalloc去找函数的地址,因为C++有名字修饰,而生成的符号表是:0x662521:_Z8tcmallocui,而C++是用_Z8tcmallocui去找的,C++可以找到,因为该动态库是C++写的,而C语言程序就找不到

那么有什么方式能让C程序和C++的程序都能用这个C++的库呢?

C++就出现了extern"C",在声明tcmalloc这个函数时在前面加extern"C" void* tcmalloc(size_t n),此时符号表按C语言修饰规则就成为了0x662521:tcmalloc,这时C程序就能正确的找到了,C++程序中有tcmaloc函数的声明,发现有extern"C",就按C语言修饰规则去找,因为C++兼容C,所有C的修饰规则它也是知道的。

总结:

当C的程序和C++的程序都想调用一个C++实现的模块时,C++可以调,但C语言不能调,那么两个都想调用时,我们就在C++实现的该模块里面函数声明时在前面加extern"C",那么生成的符号表里面就不对这个函数进行修饰了,这时C语言实现的程序就可以调用它了,紧接着C++程序在调用时,因为C++是兼容C的,发现函数的声明有extern"C",所以就在链接的时候按C的规则去找这个模块的函数

面试题:

下面两个函数能不能构成函数重载?

void TestFunc(int a = 10)
{
	cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
	cout<<"void TestFunc(int)"<<endl;
}
void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\\n", &a);
    printf("%p\\n", &ra);
}

不行,重载必须要函数参数列表不同,因为这样才在名字修饰时有所区别,能够区分函数。

C语言中为什么不能支持函数重载?

C编译器,直接用函数名关联,当函数名相同时,它无法区分

C++中函数重载底层是怎么处理的?

在链接阶段,有符号表的合并与重定义,那么函数名相同那么他们重载函数符号表会不会冲突呢?答案是不会的,C++有自己的名字修饰规则,比如重载函数参数类型一个为int,另一个为float,在名字修饰时,就会将这些信息代入进去,这样就区分了两个函数。

C++中能否将一个函数按照C的风格来编译?

可以,在该函数前面加extern"C"即可,C++程序中有函数的声明,发现有extern"C",就按C语言修饰规则去找,因为C++兼容C,所有C的修饰规则它也是知道的


引用

引用概念

引用并不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

类型& 引用变量名 = 引用实体;

#include<iostream>
using namespace std;
int main()
{
    int a=0;
    int& ra = a;//ra是a的引用,引用也就是别名,a再取了一个名称ra
    return 0;
}

我们可以重复引用,也可以对本身是引用的变量名再次进行引用,例如:

int main()
{
    int a以上是关于硬核两万字文章带你C++入门的主要内容,如果未能解决你的问题,请参考以下文章

超硬核知识:两万字总结《C++ Primer》要点!

Web 三件套两万字带你入门 CSS

Web 三件套两万字带你入门 CSS

一篇博文:带你TypeScript入门,两万字肝爆,建议收藏!

一篇博文:带你TypeScript入门,两万字肝爆,建议收藏!

两万字硬核解析树与二叉树所有基本操作(包含堆,链式二叉树基本操作及测试代码,和递归函数的书写方法)