文章目录
C++关键字
共63个,而C语言只有32个。
C++兼容C绝大多数(98%这样)的特性。
命名空间
解决命名冲突问题。
编译的时候第一步预编译就是把.h文件在.c或.cpp文件展开
- 我们自己定义的变量、函数可能和库里面的重名冲突。
- 大项目,多人协作,不同人写的代码命名冲突。
这里用到namespace
这个关键字。
用法:
namespace cjh
{
int rand;
}
这里rand
在cjh
这个名称空间里,而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;
}
函数重载需要注意的问题
- 注意必须是函数名相同,参数列表有区别才构成函数重载。返回值类型是否相同没有要求。
- 缺省值不同不能构成重载。
- 构成重载但是也可能出错。
比如下面两个函数构成重载,却无法使用。
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都代表(不是指针的指向一块空间)同一块空间。
引用需要注意的问题
-
必须在定义时初始化。
即不能
int& b;
-
一个变量可以有多个引用。
下面都是可以的
int a = 10; int& b = a; int& c = a; int& d = b;
-
引用只能引用一个实体。引用了一个实体,就不能再引用其他实体了。(注意这点和指针的区别)
即
int a = 10; int& b = a; int c = 20; b = c;//这里的作用是赋值,而不是让b变成c的别名
因此C++的引用有局限性。比如只使用引用不适用指针是无法实现链表的。
引用的应用
-
引用作参数
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,那里也可以改成引用。
在同一个作用域里面不能有同名的变量,不同作用域可以有,因此引用做参数,形参别名可以和实参是同一个名字。
-
引用作返回值
传值返回的时候,返回值会暂时放在一个临时变量中。
如果返回值比较小(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的宏,因为宏比较复杂容易出错。
内联函数的特点
-
是一种以空间换时间的做法。长函数(一般超过10行)和递归不适合作内联函数。
比如编译后是10行,
若不用内联展开,则1000次调用的指令数是1010;
若用内联展开,则1000次调用是10000个指令。
-
inline只是给编译器的一个建议(类似register),编译器未必会采用,编译器会自动优化。
-
inline不能声明和定义分离,否则会编译错误。内联函数会在调用的地方直接展开,不会生成地址,链接的时候就会报错。
直接在定义前面加inline.