C++入门基础

C++入门基础

关键字namespace命名空间

解决变量命名冲突的问题

全局作用域分成若干个部分 每个命名空间就是一个作用域

注意:命名空间不能在函数、类中等局部空间中定义

C语言中的缺陷

//在C语言中 没有很好的解决命名冲突的问题
#include<stdio.h>
#include<stdlib.h>
 
int rand=0;//库函数中定义了rand函数,此时自己重定义,会发生命名冲突

int main()
{
    printf("%d\n",rand);
    return 0;
}

C++中的

//C++中引入namespace解决
#include<stdio.h>
#include<stdlib.h>
namespace s
{
	int rand=0;    
}

int main()
{
    printf("%d\n",rand);
    return 0;
}

打印出来的并不是0 而是一个很大的数 那是rand函数地址的十进制形式

因为rand=0 是在s命名空间中的 而直接打印的是rand函数

namespace s
{
    int b=1;
}
int a=0;
int b=0;
int main()
{
    int a=1;
    printf("%d\n",a);//输出的是1 因为在函数体内 局部变量优先 
    printf("%d\n",::a);//::左侧为空  默认是全局命名空间
    printf("%d\n",s::b);//s域中的b变量
    return 0;
}

全局命名空间

除了类,函数,其他命名空间以外 也就是默认的命名空间

命名空间可以定义变量、函数、类型

namespace s{
    int a=0;
    void f()
    {
        printf("good job!!");
    }
    struct ListNode{
        int val;
        struct ListNode* next;    
    };
    
}

命名空间的嵌套

namespace Na
{
    int a=0;
    int b=0;
    int add(int left,int right)
    {
        return left+right;
    }
    namespace Nb
    {
    	int c=0;
        int sub(int left,int right)
        {
            return left-right;
        }
    }
}

命名空间可以不连续

同一工程中 允许存在多个相同名称的命名空间 编译器最后会合成同一个命名空间

//某一个文件中
namespace s
{
    int a=0;
    struct Node
    {
    	int val;
        struct Node* next;
    }}
//另一个文件中 或 同一文件中
namespace s
{
    int b=0;
    struct Node
    {
    	int val;
        struct Node* next;
    }}

由于命名空间名称相同 二者进行合并

但是二者中又存在着 相同的结构体命名 导致命名重复问题出现

所以应进行命名空间的嵌套

//某一个文件中
namespace s
{
    int a=0;//还是一个全局变量,命名空间不影响生命周期
    namespace a
    {
         struct Node
    {
    	int val;
        struct Node* next;
    }
    }
   
}
//另一个文件中 或者 同一文件中
namespace s
{
    int b=0;
    namespace b
    {
        struct Node
    {
    	int val;
        struct Node* next;
    }
    }
}

int main()
{
    struct s::b::Node n1;//s::b::Node n1 也是可以的 c++中可以省略struct 
    n1.val=2;
    s::a::Node n2;
    return 0;
}

命名空间的使用

加命名空间名称和作用域限定符

namespace s{
	int a=1;
    int b=2;
}
int main()
{
    printf("%d\n",s::a);
    return 0;
}

使用using将 命名空间中的成员引入


namespace s{
	int a=1;
    int b=2;
}

using s::a;

int main()
{
    printf("%d",a);
    return 0;
}

使用using namespace 命名空间名称引入

namespace s{
	int a=1;
    int b=2;
}

using namespace s;

int main()
{
    printf("%d",a);
    return 0;
}

所以说常见的

using namespace std;

std是C++的标准库

这句话就是将标准库引入进来

如果不引入这句话 就得使用 using std::等等

例如using std::cout;

输入和输出

#include<iostream>
using namespace std;
int main()
{
    int a;
    double b;
    cin>> a >>b;// >> 输入运算符(流提取运算符)
   	cout<<a <<b<<endl;// << 输出运算符(流插入运算符)
    cout<<"helle world"<<endl;//endl是操纵符 清空缓冲区 保证输出都在输出流 而不是在等待写入流 也就是说想输出的都可以输出 
    cout<<"hello world"<<'\n';
    return 0;
}

相对比C语言

C++可以多个同时输入输出 并且自动识别数据类型

但是按某种格式输出很麻烦 可以使用C语言 进行格式控制 (C++兼容C语言)

缺省参数(默认参数)

概念

声明定义时为函数指定一个默认值

在调用该函数时 如果没有传入对应的实参 则采用该默认值 否则使用传入的实参

(备胎的眼泪 呜呜)

void Test(int a=10)
{
    cout<<a<<endl;
}
int main()
{
    Test();//使用默认值
    Test(1);//使用传入的实参
    return 0;
}

全缺省参数

字面意思 每个参数都有默认值

void Test(int a=10,int b=20,int c=30)
{
    cout<<a<<endl;
    cout<<b<<endl;
    cout<<c<<endl;
    
}
int main()
{
    Test();//a=10,b=20,c=30
    Test(1);//a=1,b=20,c=30
    Test(1,2);//a=1,b=2,c=30
    Test(1,2,3);//a=1,b=2,c=3
    return 0;
}

半缺省参数

半缺省参数并不代表 有一半的参数有默认值 而是说 有部分参数有默认值

注意:半缺省参数必须从右向左给出,不能隔着给

因为传参从左往右传 否则传参带有歧义

void Test(int a,int b=20,int c=30)
{
    cout<<a<<endl;
    cout<<b<<endl;
    cout<<c<<endl;
}
int main()
{
    Test(1);//a=1,b=20,c=30
    Test(1,2);//a=1,b=2,c=30
    Test(1,2,3);//a=1,b=2,c=3
    return 0;
}
struct Stack
{
    int *a;
    int size;
    int capacity;
}void StackInit (struct Stack* ps,int n=4)
{
    assert(ps);
    pa->a=(int*)malloc(sizeof(int)*n);
    ps->size=0;
    ps->capacity=n;
}
int main()
{
    struct Stack st;
    //StackInit(&st);使用默认值
    StackInit(&st,20);
    return 0;
}

缺省参数在声明和定义最好不要同时出现

声明和定义都给默认参数
//test.h
void Test(int i=10)
    
//test.c
void Test(int i=20)
{
    
}

因为声明和定义可能 默认参数写的不同

编译器无法判断该使用哪一个默认参数

声明给缺省参数 定义不给缺省参数
//test.h
void Test(int a=10);
//test.c
void Test(int a)
{
    
}
int main()
{
    Test();
    Test(1);
    return 0;
}

编译通过

声明不给默认参数 定义给默认参数
//test.h
void Test(int a);
//test.c
void Test(int a=10)
{
    
}
int main()
{
    Test();//编译失败
    Test(1);
    return 0;
}

编译失败

原因在于,声明在头文件中,编译阶段,头文件就展开了,而声明未加默认参数,自然编译错误,查看函数的定义是在链接阶段

总结

可以进行声明和定义分离的函数(不是类的成员函数)

声明写默认参数 定义不要写

函数重载

概念

在同一作用域允许声明名称相同的函数,参数不同(参数个数,参数类型,顺序不同)

其中 顺序不同 指的是参数类型的顺序

void a(int i,double j)
{
    
}
void a(double j,int i)
{
    
}

光是参数名不同 不构成重载

void a( int i)
{
    
}
void a( int j)
{
    
}

光是返回值类型不同 也不构成重载

long add(int left , longt right)
{
    return left+right;
}
int add(int left , longt right)
{
    return left+right;
}
int f()
{
    return 1;
}
int f(int i=1, int j=1)
{
    return 2;
}
int main()
{
    f();//歧义 不知道是调用 全缺省函数 还是 无参函数
   
}

原理

引用

概念

引用不是新定义一个变量 而是给已经存在的变量取别称

编译器 不会给引用变量开辟新的内存空间 引用变量 和被引用变量 公用一块内存空间

正如孙悟空 又叫孙行者 齐天大圣

这三者 说的都是他一人

格式

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

int a=10;
int& b=a;//b和a得是同一类型

特性

  1. 引用在定义时必须初始化
int& b;//错误
  1. 一个变量可以有多个引用
int a=10;
int& b=a;
int& c=a;
int& d=b;
  1. 一旦引用一个实体,不能再改变
int a=10;
int b=20;
int& c=a;
int& c=b;//错误
c=b;//将b的值赋给 c也就是a 

常引用

使用引用的原则:对于原引用变量,读写权限只能不变或者缩小,不能放大

const int x=10;
int& y=20;//权限放大 不行

int a=5;
const int& b=a;//权限缩小

const int c=2;
const int& d=c;//权限不变

隐式类型转换时候也需要使用常引用 来控制权限

image-20220515152104797

image-20220515152113955

const int& f=d;

在d进行隐式类型转换时,会创建一个临时变量,将double类型转换成int类型(不是单纯的截断,因为浮点型和整数型的存储机制不同)存储到临时变量中

临时变量具有常型,也就说临时变量是被const修饰的,这也就能说明为什么一定要加const

int &f= (int)d;

再看地址

f和d的地址不同

f引用的变量并不是d, 而是一个int类型的临时变量

按道理说临时变量时使用完立刻销毁的

但是如果这个临时变量被用来初始化一个引用的话,那这个临时变量的生命周期就会被延长,直到引用被销毁

image-20220515152131587

使用场景

做参数
void Swap(int x,int y)//不改变实参
{
    int tmp=x;
    x=y;
    y=tmp;
}
void Swap(int*x,int*y)//改变实参本身
{
    int tmp=*a;
    *a=*b;
    *b=tmp;
}

void Swap(int&x,int&y)//改变实参参本身
{
    int tmp=x;
    x=y;
    y=tmp;
}

意义:

输出型参数(输出类型的参数)

image-20220515170413793

这是C语言中一道二叉树前序遍历的题

returnSize是int型指针 来记录有效的节点个数

函数内部需要使用解引用* 的操作来控制returnSzie

如果使用引用&

更方便改变returnSzie

减少拷贝,提高效率

值传递 是将实参拷贝一份给形参 (形参是实参的拷贝)

引用传递可以避免大量的数据拷贝

返回值

传值返回

int f()
{
    int n=0;
    n++;
    return n;
}
int main()
{
    int a=f();
    return 0;
}

在传值返回的时候 会产生临时变量

因为在f()函数栈帧销毁后 n也就销毁了

传引用返回

//错误示范
int& f()
{
    int n=0;
    n++;
    return n;
}
int main()
{
    int& a=f();
    return 0;
}

image-20220515172837200

a和n的地址一样

也就意味着a是n的别名

但是这么做就越界了

在f()结束时 n已经销毁了 但是还能使用a(n的别名)

相当于是 因为引用搞出来的 野指针

属于非法访问

有数据覆盖的风险

那什么时候能用引用访问呢

//正确示范
int& f()
{
    static int n=0;
    n++;
    return n;
}
int main()
{
    int& a=f();
    return 0;
}

有局部静态变量的时候 就可以使用传引用返回

因为局部静态变量虽然 作用域还是局部的

但是生命周期是全局的 也就是说出了f()函数 并没有销毁

那么它的别名自然也就是合法的 不越界的

补充

int& add(int x,int y)
{
    int z=x+y;
    return z;
}
int main()
{
    int& a=add(2,3);
    add(4,5);
    cout<<a<<endl;
    return 0;
}

输出的是9

因为a是z的别名 当调用完add(2,3)后 z已经销毁

但是调用相同的函数 在同一个地方开辟函数栈帧 所以z的空间 又继续使用了

这时候z是4+5=9 那么作为z的别名 a也就是9

int& add(int x,int y)
{
    static int z=x+y;
    return z;
}
int main()
{
    int& a=add(2,3);
    add(4,5);
    cout<<a<<endl;
    return 0;
}

这样的话 就是2+3=5了

因为静态变量的定义只执行一次

add(4,5)相当于没有产生作用

传值返回和传引用返回的区别

传值返回:会有临时变量

传引用返回:不会进行拷贝,效率更高

总的来说,函数返回时,出了函数的作用域,但返回对象还未销毁(未归还给系统),则使用引用返回和值返回都可以,但是如果已经销毁了,就只能使用值返回。(引用返回有数据覆盖的风险)

引用和指针的对比

相同

引用在语法上是别名 没有独立的空间

指针在语法中是变量 有独立的空间

但在底层视线中 实际上 两者都是相同的

也就是说 在底层实现中 引用也是有空间的

可以通过汇编代码 看简单看一下

image-20220711150717052

不同
  • 引用在定义时必须初始化,指针没有强制的语法要求
  • 引用在初始化一个实体后,不能再引用其他实体,而指针则是可以改变(在同一类型中)
  • 没有NULL引用,但有NUll指针
  • sizeof中 引用的的结果是引用类型的大小,而指针一直都是地址空间所占字节的个数(32位平台 占4字节)
  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  • 有多级指针 但是没有多级引用
  • 访问实体方式不同,指针需要显式解引用,引用则是编译器自己处理
  • 引用比指针使用起来更安全一些

内联函数(inline)

回顾宏的知识

int add(int x.inty)
{
 return x+y;   
}
//实现add的宏
#define add(x,y) ((x)+(y))//可能会忘记加括号 会导致优先级的错误
//场景举例
add(1.2);//((1)+(2))
add(1,2)*5;//((1)+(2))*5    若是外部不加括号 (1)+(2)*5
add(0&1,2|3);//((0&1)+(2|3))----(0+1)   若是内部不加括号 (0&1+2|3)  +的优先级高   (0&1&1)
宏的缺陷

1.代码可读性差 容易写错

2.不支持调试(预处理阶段进行了替换)

C++中可替代宏的

C++中内联函数可以代替宏中的函数定义

由于函数调用会开辟函数栈帧 会有一定的时间消耗

为了消除函数调用的时空开销,C++ 提供一种提高效率的方法(内联函数),即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。(利用内联函数 空间换取时间 )

而对应的,在常量方面 const可以代替宏

特点

  • inline是以空间换时间的做法,省去开辟函数栈帧的开销,所以如果说代码很长或者循环或者递归不建议使用内联
  • inline对编译器来说 只是个建议 不代表只要用inline就会替换代码 如果说代码很长 可能不会替换
  • inline函数不能声明和定义分离 会导致链接错误 因为内联函数在编译阶段是不会产生函数的地址的 它利用拷贝代码规避了开辟函数栈帧 所以链接阶段在符号表里找不到inline函数的地址

auto关键字(C++11)

简介

在C++11以前 auto修饰的变量是具有自动存储器的局部变量 但是用的很蛮少的(好像是因为非全局变量默认auto类型 用不用都一样)

在C++11之后 auto史诗级加强了 改变了原来的作用

不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

int add(int x,int y)
{
    return x+y;
}
int main()
{
    int a=1;
	auto b=a;
	auto c='c';
	auto d=add(a,a); 
}

编译器将自动推导出

b是int型,c是char型,d是int型

如果说用auto定义变量时 没有进行初始化

//例如:
auto f;

将无法通过编译 因为auto相当于一个类型声明时的占位符,在编译阶段会将auto替换成变量实际的类型。

使用细则

和指针、引用结合
int x = 5;
auto a = &x;
auto* b = &x;

auto& c = x;

cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;

用auto声明指针类型,auto和auto*没有区别,声明引用类型时必须加 &

一行定义多个变量

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

auto a=5,b=10;
auto c=2,d=2.2;//编译错误

不能使用的场景

不能作为形参

以下代码都不可以

int add(auto x,auto y)
{
    return x+y;
}
int add(int x,auto y=10)
{
    return x+y;
}

因为无法真实地推导参数

并且防止滥用 防止出现

auto add(auto x,auto y)
{
    return x+y;
}

如果是这样的话 就相当于没有类型了

不能声明数组
auto a[]={1.2.3.4};

我认为 编译器是通过数组的数据类型来开辟空间储存数据的

而auto声明的变量必须由编译器在编译时期推导而得

这就互相矛盾了

auto的意义
  • 在类型名特别长的时候 可以简化代码 让编译器自动推导
  • for的范围循环中可以使用

for的范围循环

简介

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

int a[] = { 1, 2, 3, 4, 5 };

for(auto& x: a)
x *= 2;

for(auto x : a)
cout << x << " ";

将a数组的每一个数都传给x

如果不使用引用 x只是一个拷贝 并不会改变a的本身

这里的auto 不是必须是auto 也可以是int char double等等

前提是能对应 在这里使用auto是为了方便

使用条件

循环迭代的范围是确定的
void test_for(int a[])
{
    for(auto e:a)
    {
        cout<<e<<endl;
    }
}
int main()
{
    int a[]={1,2,3,4,5};
    teat_for(a);
}

数组传参 实际上传的是指针 无法找到迭代的次数

对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围

for(beg=v.begin,end=v.end;beg!=end;beg++)//基于传统for循环
{
    auto &e=beg;
	e *= 2;
}

关键字nullptr

C程序中的NULL

在C语言中,NULL通常被定义为:#define NULL ((void *)0)

NULL实际上是一个空指针,C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。

int  *pi = NULL;
char *pc = NULL;

C++程序中的NULL

但是以上代码如果使用C++编译器来编译则是会出错的,( 因为C++是强类型语言,void是不能隐式转换成其他类型的指针的)所以实际上编译器提供的头文件做了相应的处理:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

可见,在C++中,NULL实际上是0.( 因为C++中不能把void类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。)

但是实际上,用NULL代替0表示空指针在函数重载时会出现问题

#include <iostream>
using namespace std;

void f(void* i)
{
	cout << "f1" << endl;
}

void f(int i)
{
	cout << "f2" << endl;
}

int main()
{
	f(NULL);
	f(nullptr);
    return 0;
}

在这段代码中,我们对函数f进行重载,参数分别是void类型和int类型,但是运行结果却与我们使用NULL的目的是不同的,因为本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数,而不是void为形参的函数。

C++中的nullptr

在C++11版本中引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。

总结:

NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,在C++中表示空指针应该使用nullptr更为准确

关键字nullptr

C程序中的NULL

在C语言中,NULL通常被定义为:#define NULL ((void *)0)

NULL实际上是一个空指针,C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。

int  *pi = NULL;
char *pc = NULL;

C++程序中的NULL

但是以上代码如果使用C++编译器来编译则是会出错的,( 因为C++是强类型语言,void是不能隐式转换成其他类型的指针的)所以实际上编译器提供的头文件做了相应的处理:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

可见,在C++中,NULL实际上是0.( 因为C++中不能把void类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。)

但是实际上,用NULL代替0表示空指针在函数重载时会出现问题

#include <iostream>
using namespace std;

void f(void* i)
{
	cout << "f1" << endl;
}

void f(int i)
{
	cout << "f2" << endl;
}

int main()
{
	f(NULL);
	f(nullptr);
    return 0;
}

在这段代码中,我们对函数f进行重载,参数分别是void类型和int类型,但是运行结果却与我们使用NULL的目的是不同的,因为本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数,而不是void为形参的函数。

C++中的nullptr

在C++11版本中引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。

总结

NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,在C++中表示空指针应该使用nullptr更为准确

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值