本文适合学过循环分支语句、函数、数组、结构体、指针等知识,或者是有C语言基础的同学想要进一步学习C++的同学阅读。如果循环分支、函数等等这些还不懂,不建议阅读。
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看一下C++的历史版本。
阶段 | 内容 |
---|---|
C with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
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 | 制定ing |
C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11,所有大家不用追求最新,重点将C++98和C++11掌握好,等工作后,随着对C++理解不断加深,有时间可以去琢磨下更新的特性。
C++总计63个关键字。
ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再细讲。
大家也别觉得多,其实这里面包括了C语言中的关键字
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
简单来说就是解决命名重复这个问题的
例如:
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main() {
printf("%d\n", rand);
return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
1. 正常的命名空间定义
// 1. 正常的命名空间定义
namespace a //namespace + 名字(名字你可以随便取,一般开发中是用项目名字做命名空间名。)
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
2.嵌套定义
就是命名空间里面可以再套一个命名空间:
namespace M
{
int a = 10;
int b = 20;
namespace N
{
int c = 30;
int d = 40;
}
}
为了说明解决命名冲突这个问题,可以举个非常简答的例子:你看图中我们直接定义了两个全局变量rand,按以前的来说这肯定会报重定义的错误的,但我们用了命名空间后就解决了这个问题,虽然我rand名字一样,但我一个是命名空间a里的rand,一个是命名空间b里的rand那肯定不一样啦。
#include <iostream>
using namespace std;
namespace a
{
int rand = 10;
}
namespace b
{
int rand = 20;
}
int main()
{
cout << "命名空间a中的rand = " << a::rand << endl;
cout << "命名空间b中的rand = " << b::rand << endl;
return 0;
}
1. 使用命名空间名称+ :: (作用域限定符)
符号"::"在C++中叫做作用域限定符,我们可以通过命名空间名称 :: 命名空间成员的方式访问到命名空间中的内容。
#include <iostream>
using namespace std;
namespace M
{
int a = 10;
int b = 20;
}
int main()
{
cout << M::a << endl << M::b << endl;
return 0;
}
2. 使用using将命名空间中的成员引入
例如:可以发现输出a就不需要M::a这么写了,那是因为前面使用了using M::a。但是b还得写M::b,如果b也不想这样写了,那就只能前面再加一条using M::b。
3. 使用using namespace 命名空间名称引入
这样做就是将这个命名空间中的内热全部引入了,凡是在这个命名空间中的内容都不需要通过命名空间名称+::+变量名称的方式访问了。
新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物,
那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。
#include <iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout << "Hello world!!!" << endl;
return 0;
}
在C语言中我们是使用printf和scanf来实现输出和输入的,而在C++中我们是使用cin和cout来实现输入输出的,下面是对这段程序的详细解释:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名
空间使用方法使用std。- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头
文件中。- <<是流插入运算符,>>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出
可以自动识别变量类型。- 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我
们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习
IO流用法及原理。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即 可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不 带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用<iostream>+std 的方式。
由于C++中的输入输出是不需要增加数据格式控制的,会自动识别的。不像C语言那样%d啊、%s啊等等。由此观之C++的输入输出更加方便了。
#include <iostream>
using namespace std;
int main()
{
int a=10;
float b=3.14;
char str[7] = "string";
cout << a << endl << b << endl;
cout << str;
return 0;
}
// ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因
//为C++兼容C语言的用法,这些又用得不是很多,我们这里就不展开学习了。后续如果有需要,我们再配合文
//档学习。
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值,就会输出0
Func(10); // 传参时,使用指定的实参,就会输出10
return 0;
}
全缺省参数
全缺省参数即函数的所有形参都给一个默认值,即全部都设置为缺省参数
void Print(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
半缺省参数
半缺省参数,即函数的参数不全为缺省参数,但这里有个四个规定:
1. 半缺省参数必须从右往左依次给出,不能隔着给。
void Print(int a, int b = 20, int c) //错误示例
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
2. 缺省参数不能在函数声明和定义中同时出现。
//错误示例
//test.h中声明
void Print(int a = 10, int b = 20, int c = 30);
//test.cpp中定义
void Print(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
缺省参数只能在函数声明中出现,或者函数定义时出现,二者只能其一,不可以同时出现。
3. 缺省值必须是常量或者全局变量。
4. 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;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
由于C和C++编译器对函数名字修饰规则的不同,在有些场景下可能就会出问题,比如:
在这种混合模式下开发,由于C和C++编译器对函数名字修饰规则不同,可能就会导致链接失败,在该种场景下,就需要使用extern “C”。在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。
注意:在函数前加extern "C"后,该函数就不支持重载了。
详情见我的另一篇文章引用详解,在这篇文章里我对引用进行了详细的阐述。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
非内联函数:
#include <iostream>
using namespace std;
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(10, 20);
return 0;
}
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
内联函数:
#include <iostream>
using namespace std;
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(10, 20);
return 0;
}
非内联函数的汇编:
内联函数的汇编:
没有函数调用
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,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替换为变量实际的类型。
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2. auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = {1, 2, 3, 4, 5};
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
2. 迭代的对象要实现++和= =的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现
在大家了解一下就可以了)
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
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(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
对于C++98中的问题,C++11中引入了关键字nullptr。
注意:
本篇文章就到这里就结束了,希望对各位有所帮助,喜欢的友友们别忘记三联哦!我们下篇再见,拜拜!