【初阶与进阶C++详解】第一篇:C++入门知识必备

⭐️本篇博客我要给大家分享一下C++入门知识。希望对大家有所帮助。
⭐️ 博主码云gitee链接:码云主页

目录

前言

一、C++关键字

二、命名空间

        1.命名空间定义

        作用

         介绍::

       1. 普通命名空间

        2.命名空间的嵌套定义

        3.同一个工程中可以有多个相同名称命名空间,编译器最后会合成到同一个命名空间中去

        2.命名空间的使用

三、C++输入和输出

四、缺省函数

        1.缺省参数概念

        2.缺省参数分类 

五、函数重载

        1.函数重载概念和介绍

         2.为什么C语言不支持函数重载,C++支持函数重载,C++是如何支持函数重载

        extern"C" 

六、引用

        1.引用的概念和特性

        2.常引用 

                做参数

               做返回值 

                两者比较 

                引用和指针的区别 

七、内联函数

        1.概念

        2.特性 

八、auto关键字

        1.介绍

        2.使用方法 

        3.不能使用场景

         typeid

九、基于范围的for循环

十、指针空值--nullptr

总结


前言


一、C++关键字

C++又新增了31个关键字,一共63个关键字

image-20211009155405225

二、命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的,一个命名空间就相当于定义了一个新的作用域(隔离作用),命名空间中的所有内容都局限于命名空间中,可以定义变量,定义函数。

        1.命名空间定义

定义命名空间,需要使用到namespace关键字,namespace定义的是一个域,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员,成员还是全局变量,命名空间不影响声明周期,全局变量也会分配空间,命名空间只能放在全局,命名空间不会改变变量生命周期

        作用

解决C语言命名冲突,下面就是,输出scanf时候,会优先到局部变量去寻找,不回去头文件中寻找,前面定义scanf不会报错,但后面使用的时候就会报错

int scanf =10;
int strlen = 20;

scanf ("%d", &scanf);
printf("%d\n", scanf);
printf("%d\n", strlen);

         介绍::

:: 域作用限定符号,指定访问域

定义一个域

namespace Penguins_not_bark{
    int scanf = 10;
}

使用:

int main (){
    printf("%d",Penguins_not_bark::scanf);
}

优先使用bit这一个域中的参数,并且不会与stdio.头文件中的函数发生冲突,如果只有::说明前面是全局域

       1. 普通命名空间

namespace Penguins_not_bark
{
	int a = 10;
	int b = 20;

	int Add(int x, int y)
	{
		return x + y;
	}
}

        2.命名空间的嵌套定义

namespace Penguins_not_bark
{
	int a = 10;
	int b = 20;
    namespace Penguins
 {
	 int x = 10;
	 int y = 20;

	 int Add(int x, int y)
	 {
	 	return x + y;
	 }
 }
}

        3.同一个工程中可以有多个相同名称命名空间,编译器最后会合成到同一个命名空间中去

namespace Penguins_not_bark
{
	int a = 10;
	int b = 20;

	int Add(int x, int y)
	{
		return x + y;
	}
}

namespace Penguins_not_bark
{
	int x = 10;
	int y = 20;
}

        2.命名空间的使用

        1.指定命名空间,加命名空间名称及作用域限定符,每个地方都要指定命名空间   

int main()
{
	std::cout<<"hello!"<

        2.使用using namespace 命名空间名称,相当于库里面东西都到全局域里面了,但是如果我们库里面的东西和全局域冲突的话就没办法解决了

using namespace std;//关键字using的使用将命名空间展开到全局

int main()
{
	cout<<"hello!"<

        3.对库里面的东西部分展开,引入使用using将命名空间中成员引入,这样使用变量的时候不需要加上命名空间了

using namespace std::cout;
using namespace std::endl;
int main()
{
	cout<<"hello!"<

三、C++输入和输出

使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间

使用C++输入输出更方便,不需增加数据格式控制,自动识别类型

cout和cin是函数重载和运算符重载

>> 流提取运算符    << 流插入运算符

有命名空间 , 没有命名空间

四、缺省函数

        1.缺省参数概念

缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参

缺省参数在调用的时候更加灵活

void StackInit (struct Stack* ps,int InitCapacity = 4){
    ps->a = (int*) malloc(sizeof(int)* InitCapacity) ;
    ps->size = 0;
    ps->capacity = InitCapacity;
}

int main(){
    struct Stack stl;
    //假设我知道栈里面至少要存100个数据StackInit(&st1,100);
    struct Stack st2;
    //假设我知道栈里面最多要存10个数据StackInit(&st2,10);
    struct Stack st3;
    //假设我丕知旗x果炽服众i以StackInit(&st2);
    return 0;
}

半缺省参数必须从右往左依次来给出,不能间隔着给,因为传参是从左往右
缺省参数不能在函数声明和定义中同时出现,所以声明的时候缺省参数,定义的时候不可以

//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
void TestFunc(int a = 0)
{
    cout<

        2.缺省参数分类 

全缺省,全部参数都有默认值

void TestFunc(int a = 10, int b = 20, int c = 30)
{
    cout<<"a = "<void TestFunc(int a, int b = 10, int c = 20)
{
    cout<<"a = "<五、函数重载 
  

        1.函数重载概念和介绍

C语言不可以定义相同的名字的函数,C++可以定义相同名字的函数,但是要求参数类型不同,或者参数个数不同,或者参数顺序(不同类型的形参)不同,和返回值没有关系

 虽然都是调用Add但是,会分别调用对应的类型

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;
}

         2.为什么C语言不支持函数重载,C++支持函数重载,C++是如何支持函数重载

编译连接的过程:
f.h f.cpp main. cpp
1、预处理--头文件展开+宏替换+去掉注释+条件编译

        f.i main.i
2、编译--检查语法,生成汇编代码

        f.s main.s
3、汇编―--把汇编代码转成二进制机器码

        f.o  main.o
4、链接--链接到一起生成可执行程序

        a.out

 编译后链接前,a.out的目标文件中没有Add的函数地址,因为Add是在头文件中声明的,而声明是没有创建函数栈帧,所以声明是没有地址的但是编译器还是让程序过了

call(?)

之后符号表汇总后再将这些call后面地址填好,在链接阶段完成

#include 
using namespace std;

int add(int a, int b);
double add(double a, double b);

之后链接的时候将上述文件链接到一起,同时到其他文件中取找到Add函数的地址,同时每一个目标文件都会生成一个符号表其中f.o文件的符号表中有Add函数的地址,找到函数地址的时候将函数地址填入之前编译部分跳过的部分(call),如果没有找到就会报链接错误

C++为了区分第二个相同的函数名,会用函数名修饰规则支持函数重载,那么第一个函数会变成_Z3addii(地址),第二个函数会变成_Z3adddd(地址),两个函数地址不一样。函数修饰【_Z+函数长度+函数名+类型首字母】,int* 的话,就转化为Pi

但是C语言的函数地址是直接拿函数名作为函数,编译阶段就会报错,没有前缀后缀 ,没有函数名修饰规则

C++函数名修饰规则会把参数首字母带入,所以参数不同函数名就不同

不同编译器下的修饰规则是不一样的

        extern"C" 

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。可以直接加在函数声明的前面,也可以括起来多个函数声明。

extern "C" int Add(int left, int right);
int main()
{
    Add(1,2);
    return 0;
}

但是这个函数不能重载

C++和C可以互相调用,但是java不行

//如果c++调用C那么此时所有的exextern "C"替换为EXTERN_c
#ifdef __cplusplus
    #define EXTERN_c extern "C"
//如果C调用C那么此时所有的EXTERN_C替换为什么都没有,不影响
#else
    #define  EXTERN_C
#endif

EXTERN_C void add(int a, int b);

简化法

#ifdef __cplusplus
extern "C"
    {
#endif
    int add(int a, int b);
#ifdef __cplusplus
    }
#endif

但此时不支持函数重载

六、引用

        1.引用的概念和特性

引用不是新定义一个变量,而是给已存在变量取了一个别名,它和它引用的变量共用同一块内存空间引用类型必须和引用实体是同种类型

引用在定义时必须初始化
一个变量可以有多个引用
引用一旦引用一个实体,再不能引用其他实体

int a = 10;
int& b = a;

int e = 20;
b = e;

上面b是a的引用,b = e(就是赋值),因为b和a地址一样,所以b和a的值都会变成20,但是地址还是原来的样子。

类型& 引用变量名(对象名) = 引用实体;

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

        2.常引用 

void TestConstRef()
{
const int a = 10;
//int& ra = a; // a为常量,加了const不能修改,那自己和别名都不可以修改,此行为属于权限放大
//要变成cosnt int& ra = a;

int b = 10;
int& rb = b;
const int& crb = b;//此行为可以使用,由可以改变变成不可以改变,属于权限缩小

int c = 10;
double b = 1.1;
b = c;//隐式类型转换,中间会产生临时变量(存放的是浮点数的整数部分)

const int& b = 10;
double d = 12.34;
//int& rd = d; // 权限放大,是可读可写的
const int& rd = d;//中间产生临时变量,rd为临时变量的·别名,临时变量具有常性,不能改变,所以加const(权限没有放大,权限只是只读)就不会报错了
}

                做参数

用传引用比传值要快的很多,引用和传址速度差不多,但是引用比传址更好理解,减少拷贝提高效率,输出型参数(将参数返回)。

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

               做返回值 

 传值返回,ret引用的不是C,是一个临时变量(返回值类型就是临时变量类型,函数结束,函数就销毁了,所以就拷贝给临时变量,临时变量暂时存在寄存器),临时变量具有常性(常性不能修改,不加const就是权限放大),所以要加const,同时函数返回的是C的别名

int Add(int a,int b){
    int c = a + b;
    return c;
}
int main(){
    const int& ret = Add(1,2) ;
    return 0;
}

传引用返回,下面代码是错误的,函数返回的是临时变量,不能定义临时变量的别名,临时变量具有常性

int A() {
	int n = 0;
	++n;
	return n;
}

int main ()
{

    int& ret = A();            
	cout << ret << endl;
	return 0;
}

改成下面只是不会报错,函数返回的是n的别名,ret就是n的别名,n和ret地址是一样的

但是函数结束,函数销毁,此时打印ret就相当于再次访问被销毁的空间,打印的是随机值或者是1,取决于编译器(看数据有没有被覆盖)

int& A() {
	int n = 0;
	++n;
	return n;
}

int main ()
{

    int& ret = A();            
	cout << ret << endl;
    cout << ret << endl;
    cout << ret << endl;
	return 0;
}
//输出: 1 随机值 随机值

传引用返回正确的使用方法

int& A() {
	static int n = 0;
	++n;
	return n;
}

int main ()
{

    int& ret = A();            
	cout << ret << endl;
	return 0;
}

下面打印出来的ret是 7 ,因为第一次函数调用ret是 3 ,之后再次调用,C变成了7,而ret有是C的别名,所以被修改成7(不安全)

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3,4);
    cout << "Add(1, 2) is :"<< ret <

下面的值是3,因为初始化只执行一次,ret依旧是C的别名,就算函数销毁了,C在静态区

int& Add(int a, int b)
{
    static int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3,4);
    cout << "Add(1, 2) is :"<< ret <

这样写的话,第一次调用就是7,后面调用就是随机值(第一次函数调用,在栈帧里面的数据被覆盖成7,所以取到的是7,在第二次和第一次调用中间会调用别的函数,数据被覆盖了,之后第二次调用的时候得到的是被覆盖后的值,随机值)

int& Add(int a, int b)
{
    int c = 0;
    c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3,4);
    cout << "Add(1, 2) is :"<< ret <

 传值返回是返回拷贝,传引用返回返回的是别名(临时变量),如果是以下代码,是引用,返回别名,存放到ret里面,所以输出的ret就是3

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}

int main()
{
    int ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <

 如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统里,则必须使用传值返回。

引用返回使用,当出了函数作用域,n还在就可以减少拷贝,此时使用引用返回

int& AD(){
    static int n = 0;
    ++n;
    return n;
}

int main ()
{
    int& ret = AD();
}

总之,当变量出了作用域还在的话,就可以用引用返回。

                两者比较 

传值返回会产生拷贝,传引用传参和传引用作返回值不会拷贝,直接返回变量的别名,避免深拷贝,可以提高效率

                引用和指针的区别 

int main(){
    int a = 10;

    //在语法上,这里给a这块空间取了一个别名,没有新开空间
    int& ra = a;
    ra = 20;

    //在语法上,这里定义个pa指针变量,开了4个字节,存储a的地址
    int* pa = &a;
    *pa = 20;
    return 0;
}

引用和指针的不同点:

        1.从语法上来说,引用定义一个变量的别名,指针存储一个变量的地址

        2.引用在定义时必须初始化,指针没有要求,int& ra;(×) int* p;(√)

        3.引用在初始化时引用一个实体后,就不能在引用其他实体,而指针可以在任何事之后指向任何一个同类型的实体

        4.没有NULL引用,但有NULL指针

        5.在sizeof中含义不同,引用结果为引用类型大小,但指针始终是地址空间所占字节个数(有占4个字节和8个字节),从语法角度:引用没有开辟格外空间,指针开辟4或8个字节。底层角度实验方式都是一样的。

        6.引用自加及引用实体增加1,指针自加及指针向后便宜一个类型的大小

        7.有多级指针,没有多级引用

        8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理

        9.引用比指针使用更加安全

int* p = NULL;
int*& rp = p;

 rp类型是int*, 是p的引用

七、内联函数

        1.概念

C语言为了避免小函数建立栈帧,提供宏函数,宏函数在预处理阶段展开(但是也不一定,只是一种建议,需要看此函数是否构成内联函数)。

        优点:提高性能,增强代码复用性

        缺点:不支持调试,宏函数语法复杂,容易出错,没有类安全检查

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。

release版本里面,没有call来开辟函数栈帧(优化)

在debug版本,按照以下配置,也可以将debug消除call

【初阶与进阶C++详解】第一篇:C++入门知识必备_第1张图片

【初阶与进阶C++详解】第一篇:C++入门知识必备_第2张图片

inline int Add(int left,int right){
    return left + right;
}
int main(){
    int ret = 0;
    ret = Add(1,2);
    return 0;
}

假设上面的函数有100条指令(一条汇编代码对应一条指令),调用上面的函数10次,函数栈帧总共会有110条指令(函数内有100条指令,十次调用用了10次call),inline调用总共就是1000条指令(每次调用都要展开)

指令变多意味,编译出来可执行程序变大,内存占用越多

        2.特性 

结论:平调用小函数建议定义成inline 

        1.内联是空间换时间的做法,省去函数调用的开销,所以代码很长或者有循环和递归的函数不适合使用内联函数

        2.内联函数只是一个建议,不是百分百执行,具体根据函数大小,如果函数太大或者有递归,编译器优化时会忽略内联

        3.inline不建议声明和定义分离,分离会导致链接错误,因为inline被展开了,就没有函数地址了,链接就会找不到,内联函数声明不能放在头文件,要放在源文件

        4.内联函数在不同源文件实现和调用会导致函数链接错误,符号表找不到地址

//下面是错误的代码
// F.h
#include 

using namespace std;
inline void f(int i);

// F.cpp
#include "F.h"
void f(int i)
{
    cout << i << endl;
}

// main.cpp
#include "F.h"
int main()
{
    f(10);
    return 0;
}

一个在头文件用inline,一个在主函数不用inline,会导致链接错误。

解决办法,内联函数声明和定义在同一个文件下,之后在其他文件直接调用就可以了

如果在一个文件下内联函数声明并定义了,但是又在另外一个文件声明了,会报错,因为会优先展开声明,但是展开的时候,符号表没有内联函数的地址,链接的时候会报错

 C++替换宏的方法:

        1.函数定义用内联函数

        2.常量定义用const

八、auto关键字

        1.介绍

auto(自动)修饰的变量,是具有自动存储器的局部变量,用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&,auto看的是“ = ”之后变量的类型。

int main()
{
    int a = 10;
    auto b = a;//类型声明成auto,可根据a的类型自动推导b
}

 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

void TestAuto()
{
    auto a = 1, b = 2;
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

        2.使用方法 

与指针结合使用

int main()
{
    int x = 10;
    auto a = &x;//int*
    auto* b = &x;//int*
    auto& c = x;//int
}

        3.不能使用场景

auto不能作为函数的参数,也不能作为返回值,必须要有值赋值给他,要初始化

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

 auto不能直接用来声明数组,编译器需要根据数组的类型去开辟数组的空间,而使用auto去声明数组,编译器无法推导,就不能去用auto去声明。

void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {4,5,6};
}

         typeid

打印变量类型 

int b = 10;
cout << typeid(b).name() << endl;

打印出来的是b的类型,是 int ,typeid返回的是一个字符串

九、基于范围的for循环

for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

依次取array中的数据,赋值给e,所以e只是拷贝,所以要写成auto& 将e变成array的别名

范围for的array必须是数组的地址,下面就不可以了,因为范围不确定

void TestFor(int array[])
{
    for(auto& e : array)
    cout<< e <

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)   //auto&e是引用array数组中的元素,效果是将元素乘2,直接改变原来数组
        e *= 2;
    for(auto e : array)    //自动遍历依次取出array中的元素赋值给e,直到结束,不会印象原来的数组
        cout << e << " ";
    return 0;
}

注意:可以用continue来结束本次循环,也可以用break来跳出整个循环

 条件:

        1.for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围
        2.迭代的对象要实现++和--的操作

十、指针空值--nullptr

C++中最好nullptr,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同,在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的,定义为

#define NULL ((void *)0)


NULL,宏定义为0,NULL被替换为0,所以NULL和0一样的,所以后时候在使用时,就默认八NULL当作数字0,如果要使用作为指针的话,必须对其进行强转(void *)0

#define NULL 0

总结

断断续续的看了一个星期左右才搞懂了这些知识,知识很多很杂,不仅现在学了,以后还要再次体会。

你可能感兴趣的:(#,C++初阶和进阶,c++,算法,后端,开发语言)