C++笔记——关键字、名称空间、输入输出、函数重载、引用、内联函数

C++关键字

共63个,而C语言只有32个。

在这里插入图片描述

C++兼容C绝大多数(98%这样)的特性。

命名空间

解决命名冲突问题。

编译的时候第一步预编译就是把.h文件在.c或.cpp文件展开

  1. 我们自己定义的变量、函数可能和库里面的重名冲突。
  2. 大项目,多人协作,不同人写的代码命名冲突。

这里用到namespace这个关键字。

用法:

namespace cjh
{
    int rand;
}

这里randcjh这个名称空间里,而stdlib.h里面的rand是在全局域的变量。

命名空间定义的是一个域。

在不同域里面可以定义相同名称的变量。

对于一个标识符,编译器先会去局部域找定义(优先用局部的),找不到再去全局域找定义,全局也找不到就会报错。

域作用限定符::

比如cjh::rand,则rand这个标识符取左边这个域里面定义的。(左边没有给,就默认是全局域)

需要主语域作用限定符不是C++新增的,C语言就有

int a = 0;
int main()
{
    int a = 1;
    printf("%d\n", a);//打印1
    printf("%d\n", ::a);//打印0
    
    return 0;
}

命名空间必须定义在全局。

刚刚cjh里面的rand是全局变量(放到静态区,会自动初始化为0)

在这里插入图片描述

注意函数名就是函数的地址。

注意像a = 0;这种赋值代码,只能写在代码块里面,不能写在全局,也就不能再namespace的花括号里面赋值,但是可以初始化。

全局变量在main函数之前初始化。

变量、函数、类型的定义都可以放在命名空间里面。

命名空间影响的是编译器编译的时候查找规则。

注意结构体怎么指定名称空间

把名称空间写在struct后面

namespace cjh
{
    struct Node//这里只是定义了一种结构体类型
    {
        struct Node* next;
        int val;
    };
    
    int rand;
    
    int Add(int left, int right)
    {
        return left + right;
    }
}

int main()
{
    cjh::rand = 10;
    struct cjh::Node node;//定义一个叫node结构体
    cjh::Add(1,2);
    
    return 0;
}

命名空间可以嵌套

namespace N1
{
    int a;
    namespace N2
    {
        int b;
    }
}

可以无限这样嵌套下去。

使用:

N1::N2::b = 10;

可以命名空间里面只放声明,在其他地方放定义。

多个同名的命名空间会被合并

比如在List.h里面写声明

namespace cjh
{
    struct ListNode
    {
        //...
    };
    void ListInit();
    void ListPushBack(struct ListNode* phead, int x);
}

List.cpp里面写实现

#include"List.h"
namespace cjh
{
    void ListInit()
    {
        //...
    }
    
    void ListPushBack(struct ListNode* phead, int x)
    {
        //...
    }
}

也可以这样定义(不推荐)

void cjh::ListInit()
{
    //...
}

命名空间的使用

方式1:每次使用都指定名称空间

cjh::rand

方式2:把整个命名空间展开

using namespace cjh

方式3:部分展开

using cjh::ListNode

注意不同的.cpp文件里面不能定义相同名称的全局变量。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

C++输入输出

和C的sdtio.h类似,iostream里面是C++的输入输出库函数

注意老版本的库是iostream.h(还没有命名空间语法)

C++库的实现定义在一个叫std的命名空间中。

cout是一个全局的对象

流输出(流插入)符号和流提取符号

流输出:<<

流提取:>>

cout<<"hello world"<<endl就是"hello world"流向cout,endl再流向cout

endl就是换行。

cout/cin相比printf/scanf最大特点是自动识别类型

int i = 10;
double j = 11,1;
cout << i << " " << j << endl;

cin叫流提取

cin >> i >> j;

则在控制台输入的会依次提取到i和j中(以空格\换行间隔)。也可以自定识别类型。

cout控制小数点后位数麻烦(怎么用自己了解),建议用printf。

缺省参数

void Func(int a = 0)
{
    cout << a << endl;
}

int main()
{
    Func(1);
    Func();//把0作为实参传给形参
    
    return 0;
}

全缺省

函数有多个参数,所有参数都给了缺省值。

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

注意参数只能从左往右给,也就是不能只给b的实参而其它用缺省值。

半缺省

只有部分参数给缺省值,但是只能从右往左连续缺省。

只能这样

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

不能这样

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

使用场景

struct Stack
{
    int* a;
    int top;
    int capacity;
};

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

已知capacity的大致值可以在初始化栈的时候就给capacity,可以减少增容realloc的消耗。

注意缺省参数不能在声明和定义中同时出现。

比如不能在Stack.h里面写了

void StackInit(struct Stack* ps, int capacity = 4);

在Stack.cpp里面实现时

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

缺省参数只能在声明或定义两者之一中出现。

推荐写在声明。

全局变量可以声明在.h文件,但是不能定义在.h文件。

因为.h文件会在多个.cpp文件展开,.obj文件链接的时候会出错。

函数重载

重载可以理解为一词多义的意思。

定义:C++允许在同一作用域中(全局)声明几个功能类似的同名函数,这些同名函数的**形参列表(参数个数 或 类型 或 顺序)**必须不同

类型不同

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, int right)" << endl;
    return left + right;
}

int main()
{
    Add(1, 2);
    Add(1.1, 2.2);
    
    return 0;
}

参数个数不同

void f()
{
    cout << "f()" << endl;
}

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

int main()
{
    f();
    f(1);
    
    return 0;
}

参数顺序不同

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

函数重载需要注意的问题

  1. 注意必须是函数名相同,参数列表有区别才构成函数重载。返回值类型是否相同没有要求。
  2. 缺省值不同不能构成重载。
  3. 构成重载但是也可能出错。

比如下面两个函数构成重载,却无法使用。

void f()
{
    cout << "f()" << endl;
}
void f(int a = 0)
{
    cout << "f(int a)" << endl;
}

int main()
{
    f();//这时就会报错
    f(1);//如果传参就不会造成歧义
    
    return 0;
}

函数重载原理

在这里插入图片描述

核心就是C++引入了函数名修饰规则。

C语言不支持函数重载是因为C语言的函数直接通过函数名标识和查找。

编译的时候两个重载函数函数名相同,在func.0符号表中存在歧义/冲突,其次链接的时候也存在歧义/冲突。

C++的目标文件符号表中不是直接用函数名来标识和查找函数,而是使用修饰后的函数名。

函数名的修饰规则不同编译器不同。

在Linux下(g++)规则比较简单(注意gcc编译C源文件,g++编译C++源文件)

-Z + 函数名长度 + 参数类型首字母

因此,修饰后的函数名只要参数不同func.o符号表里面的重载函数标识符就不同了。(同时可见与返回类型无关)

需要注意的是,函数如果不是定义在本源文件,那么转成汇编代码的时候call指令先不写函数地址,链接的时候在其他目标文件找到了再把地址天上去;如果函数就定义在使用这个函数的源文件,则汇编时就会把函数地址写上去。

引用

引用是为了解决指针使用复杂的问题

概念:引用不是定义新变量,而是给一个已经存在的变量取别名。

引用的定义:

&放在变量名和类型中间。

int main()
{
    int a = 10;
    int& b = a;//b就是a的引用
    int* p = &a;//取地址
    
    return 0;
}

效果是a和b都代表(不是指针的指向一块空间)同一块空间。

引用需要注意的问题

  1. 必须在定义时初始化。

    即不能int& b;

  2. 一个变量可以有多个引用。

    下面都是可以的

    int a = 10;
    int& b = a;
    int& c = a;
    int& d = b;
    
  3. 引用只能引用一个实体。引用了一个实体,就不能再引用其他实体了。(注意这点和指针的区别)

    int a = 10;
    int& b = a;
    int c = 20;
    b = c;//这里的作用是赋值,而不是让b变成c的别名
    

    因此C++的引用有局限性。比如只使用引用不适用指针是无法实现链表的。

引用的应用

  1. 引用作参数

    void swap(int* p1, int* p2)//传地址
    {
        int tmp = *p1;
        *p1 = *p2;
        *p2 = tm;
    }
    void swap(int& r1, int& r2)//传引用
    {
        int tmp = r1;
        r1 = r2;
        r2 = tmp;
    }
    void swap(int a, int b)//传值
    {
        int tmp = a;
        a = b;
        b = tmp;
    }
    

    注:上面三个函数构成重载,但是传值和传引用一旦调用就会报错。

    另一个场景比如单链表尾插

    //原版本
    void SListPushBack(SLTNode** pphead, SLDataType x)
    {
    	assert(pphead);
    	SLTNode* newnode = BuySListNode(x);
    	
    	if (*pphead == NULL)
    	{
    		*pphead = newnode;
    	}
    	else
    	{
    		//找到尾结点
    		SLTNode* tail = *pphead;
    		while (tail->next != NULL)
    		{
    			tail = tail->next;
    		}
    		tail->next = newnode;
    	}
    }
    
    //使用引用
    void SListPushBack(SLTNode*& phead, SLDataType x)
    {//phead就是plist的别名,修改plist不需要二级指针了
    	SLTNode* newnode = BuySListNode(x);
    	
    	if (phead == NULL)
    	{
    		phead = newnode;
    	}
    	else
    	{
    		//找到尾结点
    		SLTNode* tail = phead;
    		while (tail->next != NULL)
    		{
    			tail = tail->next;
    		}
    		tail->next = newnode;
    	}
    }
    
    int main()
    {
        SLTNode* plist = NULL;
        SListPushBack(plist, 1);
        SListPushBack(plist, 2);
        SListPushBack(plist, 3);
        SListPushBack(plist, 4);
        
        return 0;
    }
    

    在C++实现的数据结构的书里

    typedef struct SListNode
    {
    	SLDataType data;
    	struct SListNode* next;
    }SLTNode, *PSLTNode;//注意这种连续重命名的写法
    
    void SListPushFront(&PSLTNode phead, SLDataType x);
    
    void SListPushFront(&PSLTNode pphead, SLDataType x)
    {
    	SLTNode* newnode = BuySListNode(x);
    
    	newnode->next = phead;
    	phead = newnode;
    }
    
    int main()
    {
        SLTNode* plist = NULL;
        SListPushBack(plist, 1);
        SListPushBack(plist, 2);
        SListPushBack(plist, 3);
        SListPushBack(plist, 4);
        
        SListPushFront(plist, 5);
        SListPushFront(plist, 6);
        SListPushFront(plist, 7);
        SListPushFront(plist, 8);    
        
        return 0;
    }
    

    还有LeetCode的数组大小这种返回型参数也可以用引用。

    还有构建树那题,由于是递归构建,每一层的形参改变不影响上一层的实参,所以遍历变量i要用它的指针pi,那里也可以改成引用。

    在同一个作用域里面不能有同名的变量,不同作用域可以有,因此引用做参数,形参别名可以和实参是同一个名字。

  2. 引用作返回值

    传值返回的时候,返回值会暂时放在一个临时变量中。

    如果返回值比较小(4/8字节)就存在寄存器中,比较大就存在调用者的栈帧中。

    传引用返回

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

    传引用返回的意思是不会生成c的临时变量,直接返回c的引用。

    这里其实是错误的使用,因为c是临时变量,会造成非法访问。如果销毁栈帧不清理空间则会返回正确结果,清理了就会返回随机值。这里只是为了解释机制。

    注意这里不会报错(越界读一般不会被检测出来,但是越界写一般会被检测出来)。

    malloc开的空间free后就会立刻清理空间。

    另外还可能有这种问题

    int& Add(int a, int b)
    {
        int c  = a + b;
        return c;
    }
    int main()
    {
        int& ret = Add(1, 2);
        cout << ret << endl;//输出3
        Add(10 ,20);
        cout << ret << endl;//输出30.碰巧Add用的还是同一块空间,c用的是同一块空间
        
        return 0;
    }
    

    引用返回的意义更多的是在类和对象中体现,日常中不建议用引用返回。

    引用返回的原则:

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

    比如

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

    再比如

    int& At(int i)
    {
        static int a[10];
        return a[i];//可读可写
    }
    int main()
    {
        for(int i = 0; i < 10; ++i)
        {
            At(i) = 10 + i;
        }
        for(int i = 0; i < 10; ++i)
        {
            cout << At(i) <<" ";
        }
        cout << endl;
        
        return 0;
    }
    

由于不拷贝,传引用参数和返回引用能节省时间。

总之

引用在有些场景下可以提升性能;可以使得形参改变时实参也会改变;有些场景下引用返回可以改变返回对象。

常引用

总之就是引用后权限不能被放大,但是可以缩小。

比如

const int a = 10;
int& b = a;//权限放大,无法通过编译

const int a = 10;
const int& b = a;//权限不变,可以

int c = 10;
const int& d = c;//权限缩小,可以

常引用的一个例子

double d = 11.11;
int i1 = d;
int& i2 = d;//这一句无法通过编译
const int& i3 = d;//这一句可以

要理解原因必须先知道不同类型之间的赋值的机制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbeQMGgv-1638859844064)(C:\Users\20390\AppData\Roaming\Typora\typora-user-images\image-20211207130859465.png)]

而临时变量具有常性,不能作为右值。
在这里插入图片描述

在这里插入图片描述

关于右值和左值

一开始在右边的就是右值,在左边的就是左值,但实际上左值也可以放在右边。

因此赋值符号左边的一定是左值,但是赋值符号右边的不一定都是是右值。

可以认为右值就是不能被修改的,比如表达式产生的临时变量、常量。

int x1 = 1, x2 = 2;
int& ret = x1 + x2;//这个引用无法通过编译

结论

const Type&可以引用各种类型的对象

void StackPrint(const struct Stack& st)
{
    //...
}

指针和引用的区别

见课件。

内联函数

引入

调用函数需要建立栈帧,栈帧中要保存一些寄存器,结束后又要回复(有些寄存器存的是上一个函数的值,但是调用的这个函数也可能要用这些寄存器,可能会把数据覆盖,因此需要把这些寄存器的值先暂时保存在其他地方(栈帧中),但函数调用结束后这些数据又要从栈里面弹出来),这些操作都是有消耗的。

对于频繁调用的小函数,C语言提供宏来优化。

而C++中还可以用内联函数来优化。

比如

inline int Add(int x, int y)
{
    int ret = x + y;
    return ret;
}
int main()
{
    int ret = Add(1, 2);
    cout << ret << endl;
    
    return 0;
}

在release下,Add就不会建立栈帧了,而是会在调用它的地方展开。

debug下一般还是不会展开,但是可以配置编译器让debug下也展开,这样就可以在VS调试窗口观察了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

有了内联函数就可以不用C的宏,因为宏比较复杂容易出错。

内联函数的特点

  1. 是一种以空间换时间的做法。长函数(一般超过10行)和递归不适合作内联函数。

    比如编译后是10行,

    若不用内联展开,则1000次调用的指令数是1010;

    若用内联展开,则1000次调用是10000个指令。

  2. inline只是给编译器的一个建议(类似register),编译器未必会采用,编译器会自动优化。

  3. inline不能声明和定义分离,否则会编译错误。内联函数会在调用的地方直接展开,不会生成地址,链接的时候就会报错。

    直接在定义前面加inline.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0fUlhwBQ-1638859844069)(C:\Users\20390\AppData\Roaming\Typora\typora-user-images\image-20211207144638817.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值