目录
一.C++入门基础
1.C++的包括
2.C++发展历史
2.1 C++版本更新
年份 | C++ 标准名称 | 非正式名称 |
---|---|---|
1998 | ISO/IEC 14882:1998 | C++98 |
2003 | ISO/IEC 14882:2003 | C++03 |
2011 | ISO/IEC 14882:2011 | C++11 |
2014 | ISO/IEC 14882:2014 | C++14 |
2017 | ISO/IEC 14882:2017 | C++17 |
2020 | ISO/IEC 14882:2020 | C++20 |
2.2 C++的诟病
3. C++参考文档
多以英文版为主,我们作为程序员要学会读英文和看英文,这样更能面向世界、面向国际、跟随世界时代发展潮流。
4. C++的重要性
4.1 编程语言排行榜
4.2 C++在工作领域中的应用
5. C++学习建议和书籍推荐
5.1 C++学习难度
5.2 学习书籍推荐(主要以中后期书籍为主)
6.C++的第一个程序
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
// test.cpp
// 这⾥的std cout等我们都看不懂,没关系,下⾯我们会依次讲解
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
7. 命名空间
7.1 namespace的价值
在C语⾔项⽬中,类似下⾯程序这样的命名冲突是普遍存在的问题,这在多人开发程序项目时,因为每个人的命名习惯和命名方式不同,所以会经常遇到此类问题。C++引⼊namespace就是为了更好地处理这样的问题。
下面的程序中,rand在头文件中是一个函数,在预处理阶段中,编译器会先把头文件先展开,而代码中程序员却把rand定作为一个变量,这就跟rand函数产生了命名冲突。
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
7.2 namespace的定义
- 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{},{}中即为命名空间的成员,命名空间的结构跟结构体结构差不多,但是不同的是{}后面不加分号。命名空间中可以定义变量/函数/类型等。
- namespace本质是定义出一个域,该域跟全局域各自独立,不同的域可以定义同名变量。
#include <stdlib.h> #include <string.h> int x = 0; namespace bit { int x = 1; } void func() { int x = 2; } int main() { int x = 3; //第一种:处理方式是先局部搜索,再全局搜索 printf("%d\n", x); //第二种:直接访问命名空间里面的变量 printf("%d\n", bit::x); //第三种:左边什么都没有的话,就是默认访问全局变量 printf("%d\n", ::x); }
以上代码更能体现域,全局变量和局部变量的作用和性质,具有典型代表的作用。重点:以下其中的三种不同的表达方式分别表示不同的变量,是对这几个概念重点的理解:
- C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所以有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。例如:下面代码中命名空间bite中的rand变量是全局变量,编译时的语法查找时不会去在命名空间里查找。
- 重点:第一,::是域作用限定符。第二,若不使用命名空间的话,会默认访问的是全局变量的指针(地址),不会到命名空间去查找;反之,则使用指定的命名空间中的指针(地址)。
#include <stdio.h> #include <stdlib.h> // 1. 正常的命名空间定义 // bite是命名空间的名字,⼀般开发中是⽤项⽬名字做命名空间名。 namespace bite { // 命名空间中可以定义变量/函数/类型 int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; } void func() { printf("%d\n", bit::rand++); //此时可以体现出来rand此时是全局变量 } int main() { func(); // 这⾥默认是访问的是全局的rand函数指针 printf("%p\n", rand); // 这⾥指定bit命名空间中的rand printf("%d\n", bite::rand); bit::Add(1, 2); struct bit::Node node; const char* str = "hello world"; strlen(str); return 0; }
- namespace只能定义在全局(因为本质是为了隔离),可以嵌套定义。
#include <stdio.h> namespace bite { // 小明 namespace xiaoming { int rand = 1; int Add(int left, int right) { return left + right; } } // 小红 namespace xiaohong { int rand = 2; int Add(int left, int right) { return (left + right)*10; } } } int main() { printf("%d\n", bite::xiaoming::rand); printf("%d\n", bite::xiaohong::rand); printf("%d\n", bite::xiaoming::Add(1, 2)); printf("%d\n", bite::xiaohong::Add(1, 2)); return 0; }
- 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突——即当多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样。以下使用数据结构中栈和队列的例子来说明这个原理:
// Stack.h #pragma once #include<stdio.h> #include<stdlib.h> #include<stdbool.h> #include<assert.h> namespace bit { typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps, int n); void STDestroy(ST* ps); void STPush(ST* ps, STDataType x); void STPop(ST* ps); STDataType STTop(ST* ps); int STSize(ST* ps); bool STEmpty(ST* ps); } // Stack.cpp #include"Stack.h" namespace bit { void STInit(ST* ps, int n) { assert(ps); ps->a = (STDataType*)malloc(n * sizeof(STDataType)); ps->top = 0; ps->capacity = n; } // 栈顶 void STPush(ST* ps, STDataType x) { assert(ps); // 满了, 扩容 if (ps->top == ps->capacity) { printf("扩容\n"); int newcapacity = ps->capacity == 0 ? 4 : ps->capacity* 2; STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } ps->a = tmp; ps->capacity = newcapacity; } ps->a[ps->top] = x; ps->top++; } } // Queue.h #pragma once #include<stdlib.h> #include<stdbool.h> #include<assert.h> namespace bit { typedef int QDataType; typedef struct QueueNode { int val; struct QueueNode* next; }QNode; typedef struct Queue { QNode* phead; QNode* ptail; int size; }Queue; void QueueInit(Queue* pq); void QueueDestroy(Queue* pq); // ⼊队列 void QueuePush(Queue* pq, QDataType x); // 出队列 void QueuePop(Queue* pq); QDataType QueueFront(Queue* pq); QDataType QueueBack(Queue* pq); bool QueueEmpty(Queue* pq); int QueueSize(Queue* pq); } // Queue.cpp #include"Queue.h" namespace bit { void QueueInit(Queue* pq) { assert(pq); pq->phead = NULL; pq->ptail = NULL; pq->size = 0; } } // test.cpp #include"Queue.h" #include"Stack.h" // 全局定义了⼀份单独的Stack typedef struct Stack { int a[10]; int top; }ST; void STInit(ST* ps){} void STPush(ST* ps, int x){} int main() { // 调⽤全局的 ST st1; STInit(&st1); STPush(&st1, 1); STPush(&st1, 2); printf("%d\n", sizeof(st1)); // 调⽤bit namespace的 bit::ST st2; printf("%d\n", sizeof(st2)); bit::STInit(&st2); bit::STPush(&st2, 1); bit::STPush(&st2, 2); return 0; }
- C++标准库都放在⼀个叫std(standard)的命名空间中,这样就会将标准库封装起来,不会和程序员写的东西相冲突。
7.3 命名空间使用
- 指定命名空间访问,项⽬中推荐这种⽅式。
- using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
#include<stdio.h> namespace bite { int a = 0; int b = 1; } // using将命名空间中某个成员展开 using bite::b; int main() { printf("%d\n", bite::a); printf("%d\n", b); return 0; }
- 展开命名空间中全部成员(即暴露到全局中去),这跟展开头文件不是一个意思,所以项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
#include<stdio.h> namespace bite { int a = 0; int b = 1; } // 展开命名空间中全部成员 using namespce bite; int main() { printf("%d\n", a); printf("%d\n", b); return 0; }
8. C++输入&输出(C++可兼容C语言)
- <iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
- std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输⼊流。在内存中有整型、字符型、浮点型等等各种类型,它们通过CPU处理进行各种运算;但在其他形式的设备中,比如文件等,想要输出到控制台或终端上的话,都需要转化为字符再输出。若需要控制精度输出的话,一般都是用printf函数来控制精度,比较简单方便。
- std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
- std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。它的作用就是插入换行符并刷新流,本质上比较复杂。
#include <iostream> int main() { //流插入 std::cout << "hello world\n"; //可连续进行流插入,不同类型的可自动识别变量的类型 int i = 10; std::cout << i << '\n'<< "\n"; double d = 1.1; std::cout << d << std::endl; //其实本质上跟printf差不多 std::cin >> i >> d; std::cout << i << ":" << d << std::endl; scanf("%d%lf", &i, &d); printf("%d %lf\n", i, d); return 0; }
- <<是流插⼊运算符,>>是流提取运算符。(这两个运算符可以生动形象化为一个箭头,箭头指向的是内容流向的位置,它们可以自动识别变量的类型。)(C语⾔还⽤这两个运算符做位运算左移/右移。)
- 使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
- IO流涉及类和对象,运算符重载、继承等很多⾯向对象的知识,这里先简单认识⼀下C++ IO流的⽤法。
- cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
- ⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
- <iostream>间接包含了<stdio.h>,所以也可以使⽤printf和scanf,VS系列编译器可以这样,但在其他编译器可能会报错,比如Linux编译器。
- 通过cout和cin这些输入或输出的结果,并不是直接就输出或输入到控制台上去的,而是先到内部的一个缓冲区,类似数组,在缓冲区中要等遇到一些刷新标志,比如换行符,才能转化为字符后,才能传到控制台或终端中去。若缓冲区中有内容还未刷新出去的话,会先刷新出去,再进行输入输出数据,因此这样C++IO效率会慢很多。在IO需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码可以提⾼C++IO效率:
#include<iostream> using namespace std; int main() { // 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码 // 可以提⾼C++IO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); return 0; }
9. 缺省参数
(在数据结构的实践应用时使用缺省参数,可以避免扩容)
- 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
#include <iostream> #include <assert.h> using namespace std; void Func(int a = 0) { cout << a << endl; } int main() { Func(); // 没有传参时,使⽤参数的默认值 Func(10); // 传参时,使⽤指定的实参 return 0; }
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
#include <iostream> using namespace std; // 全缺省 void Func1(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl << endl; } // 半缺省 void Func2(int a, int b = 10, int c = 20) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl << endl; } //不可以这样跳跃传参,必须从左到右依次连续缺省,所以以下这段代码是不对的: void Func2(int a = 10, int b, int c = 20) //或void Func2(int a = 10, int b = 10, int c) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl << endl; } int main() { Func1(); Func1(1); Func1(1,2); Func1(1,2,3); //下面这句C++是不支持这样传参的,不能这么传参 Func(,2,); //或Func(,2,3); Func2(100); Func2(100, 200); Func2(100, 200, 300); return 0; }
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
// Stack.h #include <iostream> #include <assert.h> using namespace std; typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps, int n = 4); // Stack.cpp #include"Stack.h" // 缺省参数不能声明和定义同时给 void STInit(ST* ps, int n) { assert(ps && n > 0); ps->a = (STDataType*)malloc(n * sizeof(STDataType)); ps->top = 0; ps->capacity = n; } // test.cpp #include"Stack.h" int main() { ST s1; STInit(&s1); // 确定知道要插⼊1000个数据,初始化时⼀把开好,避免扩容 ST s2; STInit(&s2, 1000); return 0; }
10. 函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。而C语⾔是不⽀持同⼀作⽤域中出现同名函数的。(平时会使用特别多)
返回值不同不能作为重载条件,因为调⽤时也⽆法区分;但是参数不同是可以区分的,有类型不同的区分。
#include<iostream>
using namespace std;
// 1、参数类型不同
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, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同(本质还是类型不同)
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 fxx()
//{}
//
//int fxx()
//{
// return 0;
//}
// 下⾯两个函数构成重载
// f()但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
11. 引用
11.1引用的概念和定义
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<< 和 >>,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。
a、b、c、d的本质是指向同一个地方的地址,即为0 最经典的就是,使用引用直接进行替换。
注意:引用是给变量取别名,typedef是给类型取别名,#define是定义一个宏的常量,是替换作用。这三者本质上不一样,天差地别,要注意区分,不要混用。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
//运用到指针上同样可以
int* p1 = &a;
int*& p2 = p1;
int e = 10;
p2 = &e;
cout << *p1 << end1;//输出的结果是10
return 0;
}