赞
踩
经过了C语言的学习,我们终于可以学习另一门新的语言:C++
目录
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。
此时,计算机界提出了面向对象编程的思想,支持面向对象语言的开发。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。
C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计。
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C withclasses。
C withclasses也不断完善着自己,就这样经过不断的修修补补,诞生了C++这门语言。不过C++标准的更新肉眼可见的慢,而且这门语言虽然十分强大,但是随着更新内部的小问题也很多。
阶段 | 内容 |
C withclasses | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
C++03 | C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即计划在本世纪第一个10年的某个时间发布 |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等 |
C++17 | 在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等 |
C++20 | 自C++11以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更新,比如:Lambda 支持模板、范围for支持初始化等 |
C++23 | 制定中 |
C++还在不断的向后发展。但是,现在主流使用还是C++98和C++11,所以大家不用追求最新,重点将C++98和C++11掌握好就够了,等对C++有比较清楚的认知的时候,有时间可以去琢磨下更新的特性。
C++是一门很复杂的语言,同时也是一门十分重要的语言。不要把精通C++作为一个一年目标,应该要把学习语言作为一个持续的过程,同时要把语言运用在具体的应用场合中。
学习C++我们编写的不再是.c文件,而是.cpp文件。废话不多说,让我们进入C++的学习。
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
- #include <stdio.h>
- #include <stdlib.h>//包含rand函数的头文件,预处理阶段会展开rand函数的定义
-
- int rand = 10;
- //此时我们用已经定义的函数名作为变量会引发重定义问题
- //C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
-
- int main()
- {
- printf("%d\n", rand);
- return 0;
- }
-
- //编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
C++引入了命名空间关键字通过改变编译过程中查找数据的方式来避免重定义冲突。
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
- #include <stdio.h>
- #include <stdlib.h>
-
- namespace A//namespace是关键字,A是命名空间的名称
- {
- int rand = 10;
- //将变量rand放入命名空间,rand函数虽然依旧会储存在内存中
- //但是会改变编译过程中查找数据的方式,从而消除了重定义的错误
-
- }
-
- int main()
- {
- printf("%d\n", A::rand);//A::表示让编译器到指定的空间内寻找变量
- return 0;
- }
- //输出:10
命名空间改变查找数据规则的直观理解
(1)命名空间中的成员可以是很多东西,可以是变量,也可以是函数,还可以是类型。
- namespace bit
- {
- // 命名空间中可以定义变量/函数/类型
- int rand = 10;
- int Add(int left, int right)
- {
- return left + right;
- }
- struct Node
- {
- struct Node* next;
- int val;
- };
- }
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
(2)命名空间也可以嵌套使用
- #include<stdio.h>
- namespace N1
- {
- int a = 1;
- int b = 2;
- int Add(int left, int right)
- {
- return left + right;
- }
- namespace N2
- {
- int c = 4;
- int d = 3;
- int Sub(int left, int right)
- {
- return left - right;
- }
- }
- }
- int main()
- {
- printf("%d %d", N1::Add(N1::a, N1::b), N1::N2::Sub(N1::N2::c, N1::N2::d));
- return 0;
- }
- //输出:3 1
(3)同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
C++.h
- #include<stdio.h>
- namespace N1
- {
- int Mul(int left, int right)
- {
- return left*right;
- }
- }
C++.cpp
- #include"C++.h"
- namespace N1
- {
- int a = 1;
- int b = 2;
- int Add(int left, int right)
- {
- return left + right;
- }
- }
- int main()
- {
- printf("%d %d", N1::Add(N1::a, N1::b), N1::Mul(N1::a, N1::b));
- return 0;
- }
- //输出:3 2
(1)加命名空间名称及作用域限定符
- #include<stdio.h>
- int mian()
- {
- printf("%d\n", N::a);
- return 0;
- }
(2)使用using将命名空间中某个成员引入
- #include<stdio.h>
- using N::a
- int main()
- {
- printf("%d\n", a);
- printf("%d\n", N::b);
- return 0;
- }
(3)使用using namespace 命名空间名称引入
只适用于平常的练习中,真正写工程代码尽量不要用
- #include<stdio.h>
- using namespace N
- int main()
- {
- printf("%d\n", a);
- printf("%d\n", b);
- return 0;
- }
我们可以明显看到C++的程序更加形象,但也不好理解
- //C++
- #include<iostream>
-
- int main()
- {
-
- std::cout << "hello world" << std::endl;
- //cout相当于显示屏,cout只在std这个命名空间内表示显示屏。
- return 0;
- }
-
-
-
- //C语言
- #include<stdio.h>
- int main()
- {
- printf("hello world\n");
- return 0;
- }
(1)使用cout标准输出对象(控制台,也就是屏幕)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
(2)cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,也就是C中的'\n'。他们都包含在包含头文件中。
(3)>是流提取运算符。我们暂时不需要去完全搞清楚它们的作用,在这个程序中,你可以简单地理解为hello world的字符串流入屏幕,然后'\n'字符流入屏幕。
(4)使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
(5)实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
- #include<iostream>
- using namespace std;
- void function(int a = 10)
- {
- cout << a << ' ';
- }
- int main()
- {
- function();//此处没有传参,函数应用缺省参数的值
- function(5);//此处传参为5,函数正常运行
- return 0;
- }
- //输出:10 5
(1)全缺省参数
- void function(int a = 1, int b = 2, int c = 3)
- {
- cout << a << endl;
- cout << b << endl;
- cout << c << endl;
- }
(2)半缺省参数
- void function(int a , int b, int c = 3)
- {
- cout << a << endl;
- cout << b << endl;
- cout << c << endl;
- }
- void function(int a , int b, int c = 3)
- //左侧为普通参数,右侧为缺省参数,所以在以后普通参数也叫左参,缺省参数叫右参
- void function(int a , int b = 2, int c)
- //缺省参数不能在普通参数的左侧
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数、类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
- #include<iostream>
- using namespace std;
- int Add(int a, int b)
- {
- return a + b;
- }
- double Add(double a, double b)
- {
- return a + b;
- }
- //同名的函数可以同时存在
- int main()
- {
- int a = Add(1, 2);
- double b = Add(2.1, 4.7);
- cout << a << ' ' << b << ' ' << endl;
- return 0;
- }
- //输出:3 6.8
- #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;
- }
- int main()
- {
- Add(10, 20);
- Add(10.1, 20.2);
- f();
- f(10);
- f(10, 'a');
- f('a', 10);
- return 0;
- }
- //输出:
- //int Add(int left, int right)
- //double Add(double left, double right)
- //f()
- //f(int a)
- //f(int a, char b)
- //f(char b, int a)
我们知道C语言中同名函数是不允许存在的。为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
我不在这里细说,详见博客:程序的编译和运行_聪明的骑士的博客-CSDN博客_程序编译与运行
其中最重要的就是符号表的概念,在链接过程中会整理所有的全局变量与函数名进行统计(只要出现了就统计,包括函数声明),制成一个表,最后将其中无效的删除就制成了最终的符号表。
在C语言中,这个符号表只把函数名作为表格的内容,而C++中会根据函数的形参类型和顺序对函数名进行修饰,添加一些内容,让同名函数不再冲突。
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
引用的概念十分好理解,比如说:
我们鼎鼎大名的大作家鲁迅先生,原名周树人。我们无论使用他的笔名还是原名,都指的是这位伟大的作家。引用也是一样,就相当于给人起小名。
- int main()
- {
- int a = 10;
- int& ra = a;//<====定义引用类型
- printf("%p\n", &a);
- printf("%p\n", &ra);
- return 0;
- }
- //结果两地址相同
(1)引用在定义时必须初始化
(2)一个变量可以有多个引用
(3)引用一旦引用一个实体,再不能引用其他实体
- int a = 0;
- int b = 0;
- int& c;//只要我们定义了引用,就必须初始化
- int& c = a;//c就是a的小名
- c = a;//一旦引用就只能操作这个变量,不能改变指向
- int& d = a;//此时c、d都是a的引用
对于C++的变量而言,它们的权限只能缩小不能增大。而且,前后变量的类型是绝对对应的。
比如说:
- const int a = 10;//a为int类型的常量
- int& ra = a; //该语句编译时会出错,a为常量而ra是可以改变的int类型引用
- const int& ra = a;//正确
-
- int& b = 10; //该语句编译时会出错,b为常量
- const int& b = 10;//用const变成常量就可以了
-
- double d = 12.34;
- int& rd = d; //该语句编译时会出错,int与double类型不同
- const int& rd = d;//权限可以缩小,rd不可改变,但是d可以改变,相当于权限的缩小
你可以定义一个可修改的变量,也可以定义一个不能改变原数据的引用;但反过来就不行了。
(1)做参数
- //不使用引用
- void Swap(int* left, int* right)
- {
- int temp = *left;
- *left = *right;
- *right = temp;
- }
- //使用引用
- void Swap(int& left, int& right)
- {
- int temp = left;
- left = right;
- right = temp;
- }
C语言的传址和传值调用,传值调用如果不用返回值的话,没有办法返回结果。要想改变原数据,就需要使用指针。引用做参数就不需要用指针,也不需要用解引用的操作就可以直接操作变量。
(2)做返回值
- int& Count()
- {
- static int n = 0;
- n++;
- // ...
- return n;
- }
只有在变量出栈帧不销毁时才能用引用做返回值,下面是原因:(这里用count指代Add函数)
如果这个返回的变量储存在静态区等不会被因栈帧消除还回操作系统的空间,它就不会受到越界访问的影响。
(3)传值、传引用的效率
传值、传引用效率比较以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
- #include <time.h>
- struct A
- {
- int a[10000];
- };
- void TestFunc1(A a)
- {}
- void TestFunc2(A& a)
- {}
- void TestRefAndValue()
- {
- A a;
- // 以值作为函数参数
- size_t begin1 = clock();
- for (size_t i = 0; i < 10000; ++i)
- TestFunc1(a);
- size_t end1 = clock();
- // 以引用作为函数参数
- size_t begin2 = clock();
- for (size_t i = 0; i < 10000; ++i)
- TestFunc2(a);size_t end2 = clock();
- // 分别计算两个函数运行结束后的时间
- cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
- cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
- }
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
(1)引用概念上定义一个变量的别名,指针存储一个变量地址。
(2)引用在定义时必须初始化,指针没有要求。
(3)引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
(4)没有NULL引用,但有NULL指针。
(5)在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
(6)引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
(7)有多级指针,但是没有多级引用。
(8)访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
(9)引用比指针使用起来相对更安全。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
- #include<iostream>
- using namespace std;
- inline void Function(int a, int b)
- {
- printf("%d\n", a + b);
- }
-
- int main()
- {
- Function(2, 6);
- return 0;
- }
-
- //相当于:
-
-
- int main()
- {
- printf("%d\n", 2 + 6);
- return 0;
- }
(1)inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件(指最后的可执行程序,因为函数被展开成代码了,整个代码自然变长了)变大,优势:少了调用开销,提高程序运行效率。
这里要详细讲一下工作原理,比如说上面的Function如果不定义为内联函数,它就会和其他函数一样申请空间,建立栈帧然后在执行完操作后,将空间还回操作系统。
如大家所见,这个函数如果需要的栈帧空间不大而且还需要频繁使用的话,我们学过realloc函数,内存的申请和归还需要比较大的开销。频繁地建立和销毁栈帧都会降低程序的运行效率。
下面是调用函数的汇编代码,call就表示调用,后面括号里的是函数的地址。
我们如果使用内联函数呢?
首先需要调整VS的设置,为了方便调试,在debug版本下内联函数也是调用的。我们左键点击解决方案资源管理器的工程名(最上面),点属性。c/c++目录下:属性->常规->调试信息格式改为程序数据库,属性->常规->优化->内联函数扩展改为只适用于_inline (/Ob1)
下面是内联函数的汇编代码,此时函数的内容直接被当成代码直接执行。
(2)inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
(3)inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。最好直接在头文件中定义内联函数。
宏有很多优点:
(1)增强代码的复用性。
(2)提高性能。
也有缺点:
(1)不方便调试。(因为预编译阶段进行了替换)
(2)导致代码可读性差,可维护性差,容易误用。
(3)没有类型安全的检查 。
C++中于是就引入了内联函数,达到了便于书写和高效的结合
- //内联函数
- inline int Function(int a, int b)
- {
- return a+b;
- }
- //宏
- #define ADD(X,Y) ((X)+(Y))
随着程序越来越复杂,程序中用到的类型也越来越复杂,有的是类型难于拼写,有的是含义不明确导致容易出错。
你可能觉得int、float、double甚至struct A这种变量类型也不长啊,小伙子还是太年轻了。
std::mapstd::string, std::string>::iterator it = m.begin();
it这个变量的类型是std::mapstd::string, std::string>::iterator,是不是感觉直接心态爆炸。
不过没关系,在C++11中引入了auto关键字,有了auto就方便了。
auto可以根据变量后面初始化的内容来推断该变量的类型
我们就可以直接改成:auto it = m.begin();
直接起飞
(1)auto要想使用必须初始化
(2)不止auto,auto*和auto&也可以使用,只不过一个代表变量为指针,一个代表引用
(3)auto可以同时定义和声明多个变量,但是前后的变量必须类型一致
比如说:auto a = 0,b = 2;前后变量都为int类型,正确;auto a = 0,b = 2.0;前后变量一个为int类型,另一个为double类型,错误。
(4)auto不能作为函数的参数类型
例如:void function(auto a){}就是错误的
(5)auto不能定义数组
例如:auto arr[3] = {1,2,3};就是错误的
- #include<stdio.h>
- int main()
- {
- int arr[]={1,2,3,4,5,6,7,8,9,10};
- int i = 0;
- for (i = 0; i<sizeof(arr)/sizeof(arr[0]); i++)
- {
- printf("%d ",arr[i]);
- }
- return 0;
- }
用之前C语言的方式遍历数组需要用到循环,,要写老长一段。而且这个程序还只能遍历int类型的数组,适应性还很低。
- #include<iostream>
- int main()
- {
- int arr[] = {1,2,3,4,5,6,7,8,9,10};
- for(auto e : arr)
- std::cout << e << " ";
- return 0;
- }
- //输出:1 2 3 4 5 6 7 8 9 10
你没看错,只要两行。auto a相当于根据数组原来的内容建立的临时变量,然后直接输出到屏幕,arr限定大一i你的数据在arr这个数组范围内。
不过注意,原来的for循环可以改变原来的数据,但是这里打印的是同一个值的另一个临时变量e,所以我们不能这样改变原数据。
要想改变原数据,可以用引用的方式:
- #include<iostream>
- int main()
- {
- int arr[] = {1,2,3,4,5,6,7,8,9,10};
- for(auto& e : arr)
- e=*2;
- for(auto e : arr)
- std::cout << e << " ";
- return 0;
- }
- //输出:2 4 6 8 10 12 14 16 18 20
我们知道,在C语言中指针的空值是NULL,空指针为假且不能解引用。
C++中本来也沿用了这个规则,但是C++的开发者们还是自己亲手写了个bug。
其实NULL是一个宏,定义在stddef.h,下面是C++中NULL的定义:
- #ifndef NULL
- #ifdef __cplusplus
- #define NULL 0
- #else
- #define NULL ((void *)0)
- #endif
- #endif
注意,此时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;
- }
- //输出:
- //f(int)
- //f(int)
- //f(int*)
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0
咱们可能会说,有bug修复掉就可以了。
但是这可是一个编程语言的发行版本,是被全世界的人们使用的,有些代码可能正好利用了这个特点来进行代码编写。所以这个bug你想修还不能修,只能通过打补丁的方式去弥补。
一门语言需要向下兼容,而所谓向下兼容就是以前做好的程序能够直接运行在最新的平台上。
举个例子:Python2更新Python3更改了语法,导致了原来Python2的代码无法运行在Python3上。相当于Python这门语言你还需要从头学起。这是就是Python主动放弃了兼容性。Python当年被喷的要多惨有多惨。
由于NULL对整形0和空指针的混淆,C++中又添加了nullptr表示原来空指针NULL,从而解决了这个问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。