一、C++前言
1.C++的概念:
一种高级编程语言,由C发展而来,满足过程化程序设计、基于对象的程序设计、面向对象的程序设计。
2.C++发展历程
C++最新发展:LINK
3.C++如何学?
- 写博客总结
- 定期画思维导图总结
- 常复习(博客笔记、知乎答案、看书)
- 刷题
二、C++入门
1.C++关键字(C++98标准)
2.命名空间
C++命名空间概念:一种命名域、是C++专门用来解决C语言变量命名冲突问题而产生的。
命名空间的意义:很大程度上解决C变量命名冲突问题。
知识拓展:C中的命名冲突
命名冲突主要有两大类:
- 程序员 与 库
- 程序员 与 程序员
知识拓展1:域,C++上指的是作用域LINK
在C++中,大体有四种域:
- 全局域:生命周期 访问
- 局部域:生命周期 访问
- 命名空间域:访问
- 类域
知识拓展2:编译器对名称的搜索原则:
- 在不指定域的情况下
- 当前域
- 全局域
- 在指定域的情况下
- 直接去指定域搜索
命名空间的定义: namespace关键字+命名空间的名字+{…}
- 命名空间中可以定义变量/函数/类型
- 命名空间可以嵌套
- 同一命名空间会被合并
namespace bit { // 命名空间中可以定义变量/函数/类型 int rand = 10; int Add(int left, int right) { return left + right; } 注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
命名空间的使用:
- 法1:指定访问
需要用到访问操作符 ::
- 访问变量/函数 命名空间名称 + :: + 变量名/函数名
- 访问结构体 struct + 空间名 + :: + 结构体名
- 法2:直接访问(展开访问)
需要用到指令:using namespace + 空间名 —>等价于把空间内的命名放到全局变量
- 法3:展开特定项
eg:using std::cout;
总结:对比指定访问与展开访问,分析其利弊
答:
- 指定访问 解决变量冲突问题 麻烦
- 展开访问 直接把变量放到全局中 方便
- 部分展开 把常用的变量放到全局中 方便
其中,做项目一般使用第1与第3方式,第2种方法一般在练习中使用。
头文件展开与命名空间展开的区别:
头文件的展开本质是在预处理阶段拷贝头文件内容。
命名空间的展开本质是影响编译器的搜索规则。
3.C++输入&输出
①概念说明
cpp的输入输出分别是cout与cin,这两个函数均包含在iostream头文件中,在使用时需要展开命名空间进行使用。
②使用说明
#include<iostream> using namespace std; int main() { // <<,1.左移 int a = 10; a = a << 1; printf("a<<1:%d\n", a); // <<,2.流插入 cout << "hello cpp" << endl; // >>,1.右移 a = a >> 1; printf("a>>1:%d\n", a); // >>.2.流提取 cin >> a; return 0; }
③特征说明
下面是cout的特征说明示例:cin也是同理
④细节拓展
⑤cout与cin的意义
弥补了C语言自定义类型输入输出麻烦的问题
4.缺省参数
①概念说明
缺省参数是c++提供的一种在函数缺少传入值的情况下使用默认值的一种参数。
缺省参数分为两类
- 全缺省参数
- 半缺省参数
②使用规则
- 有实参优先使用,没有实参使用默认参数
- 缺省参数函数传实参时候从左到右依次给值,缺省参数函数形参从右向左依次给默认值
- 缺省参数不能在声明与定义中同时出现,要优先给函数声明使用缺省
- 缺省值必须是常量值或者全局变量
思考:为什么缺省参数要优先在函数声明中使用?而不是优先给函数定义使用?
答:
我们首先要明白编译器的编译原理,编译器首先要进行预处理,在这一步中展开头文件,之后编译器要进行编译,在这一步中编译器主要是检查语法,通常情况下,这种时候每个源文件都是独立的,每个调用函数的源文件中也自然大概率没有函数的定义,但是已经有了函数声明(因为经过了预处理),所以为了要让编译器不报错,肯定是先让函数声明有缺省参数。
那为什么不直接函数定义也有缺省参数,函数声明也有缺省参数呢?
因为语法是规定只能有一个的。
③应用举例
在c语言阶段,我们设计一个栈时候往往初始化不知道要给几个空间,这样我们可以给一个缺省参数来默认给值,然后不够了的话扩容。
④缺省参数的意义
对函数定义更加灵活化。
5.函数重载
①概念
函数名相同,函数参数不同,称为函数重载
函数参数不同在哪里?
- 参数的类型不同
- 参数的个数不同
- 参数的顺序不同
思考:编译器是如何支持函数重载的?C语言就不行。
答:名称修饰
6.引用
①概念
引用:底层汇编层面是用指针实现的,但是在语法层面上是为变量起别名,与指针起到互补作用。
②特性
- 引用在定义时必须初始化
- 引用不能更改指向
- 一个变量可以有多个引用
思考:引用可以完全替代指针吗?
答:不能,引用往往是在用指针比较复杂的地方代替指针。
为什么引用不能完全替代指针?
虽然引用可以在大多数情况下替代指针的使用从而简化指针,但是某些情况下是不能用引用解决的。
比如下面这个例子:
现在有一个单链表,要求删除中间的一个结点。
这里的关键点就在于引用不能改变指向,现在你把中间结点删除了,但是前面结点的指向怎么办?
//例子,链表 typedef int SLTDataType; typedef struct SListNode { SLTDataType val; struct SListNode* next; }SLNode, * PSLNode; //指针的方法 void SLPush(SLNode** pphead, SLTDataType x) { assert(pphead); SLNode* newnode = (SLNode*)malloc(sizeof(SLNode)); newnode->next = NULL; newnode->val = x; if (*pphead == NULL) { *pphead = newnode; return; } else { SLNode* pcur = *pphead; while (pcur->next) { pcur = pcur->next; } pcur->next = newnode; } } //引用的方法 void SLPush(SLNode*& pphead , SLTDataType x) { assert(pphead); SLNode* newnode = (SLNode*)malloc(sizeof(SLNode)); newnode->next = NULL; newnode->val = x; if (pphead == NULL) { pphead = newnode; return; } else { SLNode* pcur = pphead; while (pcur->next) { pcur = pcur->next; } pcur->next = newnode; } } void SLPrint(SLNode*& phead) { while (phead) { cout << phead->val << " "; phead = phead->next; } } void test_3() { SLNode* phead = NULL; SLPush(&phead, 1); SLPush(phead, 2); SLPush(&phead, 3); SLPush(phead, 4); SLPrint(phead); }
③应用场景
1.做参数
void C_Swap(int* a,int* b) { int temp = *a; *a = *b; *b = temp; } void CPP_Swap(int& a2, int& b2)//在这里可以起与实参一样的名字吗?可以。因为属于不同的域。 { int temp = a; a = b; b = temp; } void test_1() { int a1 = 10; int b1 = 20; printf("a1 = %d , b1 = %d\n", a1, b1); C_Swap(&a1,&b1); printf("a1 = %d , b1 = %d\n", a1, b1); int a2 = 10; int b2 = 20; cout << "a2 = " << a2 << " b2 = " << b2 << endl; CPP_Swap(a2, b2); cout << "a2 = " << a2 << " b2 = " << b2 << endl; }
思考:在这里可以起与实参一样的名字吗?
答:可以,因为属于不同的域。
在做函数参数这一用法上,用引用可以有两大好处
- 1.引用做输出型参数,更加方便
输出型参数:能够通过改变形参影响实参。
- 2.引用在对象比较大情况下,用引用代替值拷贝更加高效
void testfunc1(A a) { ; } void testfunc2(A& a) { ; } void test_4() { A a; //以值作为函数参数 size_t begin1 = clock(); for (int i = 0; i < 10000; i++) { testfunc1(a); } size_t end1 = clock(); //以引用作为函数参数 size_t begin2 = clock(); for (int i = 0; i < 10000; i++) { testfunc2(a); } size_t end2 = clock(); printf("%zd ", end1 - begin1); printf("%zd ", end2 - begin2); }
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
2.做返回值
- 1错误的引用做返回值用法
如何规避函数的野引用?
答:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
那该如何正确使用引用来返回函数值呢?
一般是返回静态变量,全局变量,堆上的空间…等等。
- 2.正确的引用返回—修改返回对象
对比C中的get函数与c++中的get函数
读下面代码。
get如果返回int,那么就是一种值拷贝,如果是int&,那么就是别名。不但可以读还可以写。
struct SLTNode { int* a; int size; int capacity; }; void SLTInit(struct SLTNode& sl) { sl.a = NULL; sl.capacity = sl.size = 0; } void SLTPush(struct SLTNode& sl,int x) { //...扩容 if (sl.size == sl.capacity) { int* psl = (int*)realloc(sl.a, 10*sizeof(int)); sl.a = psl; sl.capacity+=10; } //检查 sl.a[sl.size++] = x; } void SLTPrint(struct SLTNode& sl) { int i = 0; for (int i = 0; i < sl.size; i++) { cout << sl.a[i]; } } void SLTModify(struct SLTNode& sl,int pos,int x) { sl.a[pos] = x; } void test_C() { //例子:顺序表 struct SLTNode sl; SLTInit(sl); SLTPush(sl, 1); SLTPush(sl, 2); SLTPush(sl, 3); SLTPush(sl, 4); SLTPrint(sl); } void test_CPP() { struct SeqList { // 成员变量 int* a; int size; int capacity; // 成员函数 void Init() { a = (int*)malloc(sizeof(int) * 4); // ... size = 0; capacity = 4; } void PushBack(int x) { // ... 扩容 a[size++] = x; } // 读写返回变量 int& Get(int pos)//这个地方如果返回int,那么就是一种值拷贝,如果是int&,那么就是别名。不但可以读还可以写。 { assert(pos >= 0); assert(pos < size); return a[pos]; } int& operator[](int pos) { assert(pos >= 0); assert(pos < size); return a[pos]; } }; struct SeqList a; a.Init(); a.PushBack(1); a.PushBack(2); a.PushBack(3); a.PushBack(4); for (int i = 0; i < 4; i++) { cout << a.Get(i) << " "; } } void test_5() { //test_C(); test_CPP(); }
- 3.正确的引用返回—减少拷贝提高效率
与引用做参数是一样的,这里不做细细说明。
#include <time.h> struct A{ int a[10000]; }; A a; // 值返回 A TestFunc1() { return a;} // 引用返回 A& TestFunc2(){ return a;} void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; }
④引用和指针的区别
- 从语法层面来看
- 引用是别名,不需要额外开空间,指针需要额外开空间使用
- 引用必须初始化,指针可以选择不初始化
- 引用不能改变防线,指针可以任意改变方向
- 引用相对安全,不容易写出问题;指针容易写出野指针
- sizeof,++,解引用等方面的区别
- 从底层层面来看
- 从汇编层面上,没有引用,引用都会被转换为指针。
⑤引用的意义
引用不是用来替代指针的,而是弥补指针在有些地方的不适用,比如二级指针、三级指针过于复杂,比如指针作参数,作返回值修改不够方便等等问题。
7.内联函数
内联函数概念
概念:
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
下面为内联函数和一般函数的比较:一般函数建立了栈帧,内联函数没有建立栈帧。
int add1(int a,int b) { return a + b; } inline int add2(int a,int b) { return a + b; } void test_5() { //假设add函数需要大量重复调用 int a = 10; int b = 20; //一般函数add1 int c = add1(a, b); //内联函数add2对照 c = add2(a, b); }
注:如果想要看到上面对比图需要对编译器优化进行配置
内联函数特性
- inline是在编译阶段直接函数调用替换为函数体,也就是一种以空间换时间的方法
- 好处:少了调用开销
- 缺点:会使目标文件变大
- inline对于编译器只是一个建议,实现机制取决于编译器。
- 如果是递归函数、较大的函数编译器会忽略inline
- inline不建议声明与定义相分离,因为可能会导致链接错误
- 这是因为inline没有产生函数地址在变量列表中。
// 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; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
内联函数意义
解决某一功能需要大量重复调用,造成栈频繁开辟销毁降低性能的问题。
一般来说,C语言解决这一问题是搞一个宏函数,虽然解决了问题,但是还有一些其他问题,比如语法复杂,不能调试,没有类型安全检查等等。C++为了解决这个问题,创建了新的语法,内联函数。
如果一个函数定义在了.h文件中该如何解决这个问题?
注:inline内敛,内联函数不会把其地址加载到符号表中,自然也不会引起链接错误。
内联函数的认识
那我们该如何看待或者使用内联函数的呢?
建议:
在我看来内联函数就是为了解决体积小且大量重复的函数调用而诞生的,如果今后碰到一些大量重复调用且体积小的函数,可以考虑加上inline
但是切忌不可每个函数都加上inline
- 如果函数过大或者函数是递归函数的话 ---->编译器会忽略inline关键字
- 如果体积过大会大大增加代码,从而造成“代码膨胀”的问题。
8.auto关键字
auto概念
是C++关键字之一,用来自动为变量匹配类型,而auto类型本质是一种新类型。
int TestAuto() { return 10; } int main() { int a = 10; auto b = a; auto c = 'a'; auto d = TestAuto(); cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化 return 0; }
注:auto是一种类型声明时的“占位符”,编译器在编译期间会将auto替换为实际类型。
auto特性
- auto使用必须初始化
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则须加& - 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。 - auto不能直接用来声明数组
- auto过度使用会使可读性变差
auto意义
类型越来越复杂,难拼写,易出错 —> typedef —> auto
C语言中typedef的问题:
suto的认识
auto可以在面对较复杂的类型时候用一次,切忌不可连用,因为这会大大降低代码的可读性。
9.for的范围用法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for(auto& e : array) e *= 2; for(auto e : array) cout << e << " "; return 0; }
注意
- or循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 - 迭代的对象要实现++和==的操作
10.NULL与nullptr
C++中NULL的问题
NULL的字面值是0,默认是int类型的,这会有bug存在。
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { f(0); f(NULL); f((int*)NULL); return 0; }
注:nullptr不需要包头文件,因为他是关键字,且在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
EOF