目录
🍍3.同一个工程中可以有多个相同名称命名空间,编译器最后会合成到同一个命名空间中去
🏆2.为什么C语言不支持函数重载,C++支持函数重载,C++是如何支持函数重载
前言
💎一、C++关键字
🧡C++又新增了31个关键字,一共63个关键字
💎二、命名空间
🧡在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的,一个命名空间就相当于定义了一个新的作用域(隔离作用),命名空间中的所有内容都局限于命名空间中,可以定义变量,定义函数。
🏆1.命名空间定义
🧡定义命名空间,需要使用到namespace关键字,namespace定义的是一个域,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员,成员还是全局变量,命名空间不影响声明周期,全局变量也会分配空间,命名空间只能放在全局,命名空间不会改变变量生命周期。
🍍作用
🧡解决C语言命名冲突,下面就是,输出scanf时候,会优先到局部变量去寻找,不回去头文件<stdio.h>中寻找,前面定义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!"<<std::endl;
//这里我们可以理解为作用域限定符"::"的使用,让我们在lin这个命名空间中找到了a这个变量
return 0;
}
2.使用using namespace 命名空间名称,相当于库里面东西都到全局域里面了,但是如果我们库里面的东西和全局域冲突的话就没办法解决了
using namespace std;//关键字using的使用将命名空间展开到全局
int main()
{
cout<<"hello!"<<endl;
return 0;
}
3.对库里面的东西部分展开,引入使用using将命名空间中成员引入,这样使用变量的时候不需要加上命名空间了
using namespace std::cout;
using namespace std::endl;
int main()
{
cout<<"hello!"<<endl;
return 0;
}
💎三、C++输入和输出
🧡使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间
🧡使用C++输入输出更方便,不需增加数据格式控制,自动识别类型
🧡cout和cin是函数重载和运算符重载
🧡>> 流提取运算符 << 流插入运算符
🧡<cstring>有命名空间 , <string.h>没有命名空间
💎四、缺省函数
🏆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<<a<<endl;
}
int main()
{
TestFunc(); // 没有传参时,使用参数的默认值
TestFunc(10); // 传参时,使用指定的实参
}
🏆2.缺省参数分类
🧡全缺省,全部参数都有默认值
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
🧡半缺省,缺省部分参数 ,但是缺省的参数必须从右往左,并且是连续的,半缺省传值必须至少传一个
void TestFunc(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
💎五、函数重载
🏆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 <iostream>
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) ;//不加const就错了 return 0; }
🧡传引用返回,下面代码是错误的,函数返回的是临时变量,不能定义临时变量的别名,临时变量具有常性,要加上const才对
int A() { int n = 0; ++n; return n; } int main () { int& ret = A(); //正确写法,const 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; }
🧡C是一个局部变量,出作用域就销毁了,第一次调用,ret指向了一个被销毁的空间,打印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 <<endl; cout << "Add(1, 2) is :"<< ret <<endl; return 0; } //输出:7 随机值
🧡下面的值是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 <<endl; return 0; }
🧡传值返回是返回拷贝,传引用返回返回的是别名(临时变量),如果是以下代码,是引用,返回别名,存放到ret里面,所以输出的ret就是3,但仍然是错误,因为返回的C是一个局部变量,出作用域后销毁了
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 <<endl; cout << "Add(1, 2) is :"<< ret <<endl;//输出3 3 return 0; }
🧡如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统里,则必须使用传值返回。
🧡引用返回使用,当出了函数作用域,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
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 <iostream>
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 <<endl; }
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
💎总结
🧡断断续续的看了一个星期左右才搞懂了这些知识,知识很多很杂,不仅现在学了,以后还要再次体会。