C++ 入门基础之一
大纲
C++ 简介
C++ 介绍
- C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。
- C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。
- C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。
- C++ 是由 Bjarne Stroustrup 于 1979 年在新泽西州美利山贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,最初命名为带类的 C,后来在 1983 年更名为 C++。
注意:使用静态类型的编程语言是在编译时执行类型检查,而不是在运行时执行类型检查。
ANSI 标准
ANSI 标准是为了确保 C++ 的便携性 —— 您所编写的代码在 Mac、UNIX、Windows、Alpha 计算机上都能通过编译。由于 ANSI 标准已稳定使用了很长的时间,所有主要的 C++ 编译器的制造商都支持 ANSI 标准。
标准 C++ 的三大组成部分
- 核心语言,提供了所有构件块,包括数据类型、变量、常量等。
- C++ 标准库,提供了大量的函数,用于操作文件、字符串等。
- 标准模板库(STL),提供了大量的方法,用于操作数据结构等。
第一个 C++ 程序
1 | // 包含C++的头文件 |
程序运行的输出结果如下:
1 | hello world |
关于 endl
与 \n
的区别:
- 在 C++ 中,终端输出换行时,用
cout << ... << endl
与\n
都可以,但二者有小小的区别,用endl
时会刷新缓冲区,使得栈中的东西刷新一次;但用\n
则不会刷新,它只会换行,栈内的数据没有变化。一般情况,二者的这点区别是很小的,在大型的程序中可能会用到,建议用endl
来换行。 endl
除了写入\n
之外,还会调用flush
函数来刷新缓冲区,将缓冲区里的数据写入文件或屏幕,若考虑效率则可以直接使用\n
cout << endl;
等价于cout << '\n' << flush;
程序设计方法介绍
面向过程的程序设计方法
设计思路
面向过程的结构化程序设计方法,自顶向下、逐步求精。采用模块分解与功能抽象,自顶向下、分而治之。
程序结构
- 按功能划分为若干个基本模块,形成一个树状结构。
- 各模块间的关系尽可能简单,功能上相对独立;每一模块内部均是由顺序、选择和循环三种基本结构组成。
- 其模块化实现的具体方法是使用子程序。
优缺点
优点:
- 有效地将一个较复杂的程序系统设计任务分解成许多易于控制和处理的子任务,便于开发和维护。
缺点:
- 可重用性差、数据安全性差、难以开发大型软件和图形界面的应用软件
- 把数据和处理数据的过程分离为相互独立的实体。
- 当数据结构改变时,所有相关的处理过程都要进行相应的修改。
- 每一种相对于老问题的新方法都要带来额外的开销。
- 图形用户界面的应用程序,很难用过程来描述和实现,开发和维护也都很困难。
面向对象的程序设计方法
C++ 完全支持面向对象的程序设计,包括面向对象开发的四大特性: 封装、抽象、继承、多态
,更多特性如下:
- 将数据及对数据的操作方法封装在一起,作为一个相互依存、不可分离的整体(对象)。
- 对同类型对象抽象出其共性,形成类。
- 类通过一个简单的外部接口,与外界发生关系。
- 对象与对象之间通过消息进行通信。
面向对象的软件工程概述
面向对象的软件工程是面向对象方法在软件工程领域的全面应用,分别包括:
- 面向对象的分析(OOA)
- 面向对象的设计(OOD)
- 面向对象的编程(OOP)
- 面向对象的测试(OOT)
- 面向对象的软件维护(OOSM)
面向过程程序设计:数据结构 + 算法,主要用于解决科学计算问题,用户需求简单而固定,其特点和劣势如下:
特点:
- 分析解决问题所需要的步骤
- 利用函数实现各个步骤
- 依次调用函数解决问题
劣势:
- 软件可重用性差
- 软件可维护性差
- 构建的软件无法满足用户需求
面向对象程序设计:由现实世界建立软件模型,将现实世界中的事物直接映射到程序中,可直接满足用户需求,其特点和优势如下:
特点:
- 直接分析用户需求中涉及的各个实体
- 在代码中描述现实世界中的实体
- 在代码中关联各个实体协同工作解决问题
优势:
- 构建的软件能够适应用户需求的不断变化
- 直接利用面向过程方法的优势而避开其劣势
计算圆形的面积
面向过程的写法
1 |
|
面向对象的写法
1 |
|
C++ 基础概念
命名空间的概念
所谓 namespace
,是指标识符的各种可见范围。C++ 标准程序库中的所有标识符都被定义于一个名为 std
的 namespace
中。<iostream>
和 <iostream.h>
格式是不一样的,前者没有后缀,实际上在编译器 include
文件夹里面可以看到,二者是两个文件,打开文件就会发现,里面的代码是不一样的。后缀为 .h
的头文件 C++ 标准已经明确提出不再支持了,早些的实现将标准库功能定义在全局命名空间里,即声明在带 .h
后缀的头文件里;C++ 标准为了和 C 区别开,也为了正确使用命名空间,规定头文件不再使用后缀 .h
。<iostream.h>
与 <iostream>
的区别:
- 当使用
<iostream.h>
时,相当于在 C 中调用库函数,使用的是全局命名空间,也就是早期的 C++ 实现 - 当使用
<iostream>
的时候,该头文件没有定义在全局命名空间,必须使用using namespace std;
这样才能正确使用cout
等关键字
由于 namespace
的概念,使用 C++ 标准程序库的任何标识符时,可以有以下三种写法可选择:
直接指定标识符
:例如std::cout
而不是cout
,完整语句为:std::cout << std::hex << 3.4 << std::endl;
使用 using 关键字
:using std::cout; using std::endl; using std::cin;
,以上语句可以写成cout << hex << 3.4 << endl;
使用 using namespace std
:这种写法是最方便的,例如:using namespace std;
,以上语句可以写成cout << hex << 3.4 << endl;
命名空间 std
内定义的所有标识符都有效(曝光),就好像它们被声明为全局变量一样,那么以上语句就可以这样写 cout << hex << 3.4 << endl;
。因为标准库非常的庞大,所以程序员在选择的类名称或函数名时就很有可能和标准库中的某个名称相同。因此为了避免这种情况所造成的名称冲突,就把标准库中的一切都被放在名称空间 std
中。但这又会带来了一个新问题,无数原有的 C++ 代码都依赖于使用了多年的伪标准库中的功能,它们都是在全局命名空间下的。所以就有了 <iostream.h>
和 <iostream>
等等这样的头文件,一个是为了兼容以前的 C++ 代码,另一个是为了支持新的标准。命名空间 std
封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加后缀 .h
。
命名空间的定义
在 C++ 中,名称(name)可以是符号常量、变量、宏、函数、结构体、枚举、类和对象等等。为了避免在大规模程序的设计中,以及在程序员使用各种各样的 C++ 库时,这些标识符的命名发生冲突,标准 C++ 引入了关键字 namespace
(命名空间 / 名字空间 / 名称空间 / 名域),这样就可以更好地控制标识符的作用域。std
是 C++ 标准命名空间,C++ 标准程序库中的所有标识符都被定义在 std
中,比如标准库中的类 iostream
、vector
等都定义在该命名空间中,使用时要加上 using
声明(如 using namespace std
) 或者 using
指示(如 std::string
、std::vector<int>
)。
命名空间的使用语法
C 语言中的命名空间:
- 标识符之间可能发生冲突
- 在 C 语言中只有一个全局作用域
- C 语言中所有的全局标识符共享同一个作用域
C++ 中的命名空间:
- 命名空间可以起别名
- 命名空间可以相互嵌套定义
- 全局作用域也叫默认命名空间
- 命名空间必须定义在全局作用域下
- 命名空间将全局作用域分成不同的部分
- 不同命名空间中的标识符可以同名而不会发生冲突
- 在命名空间内可以放常量、变量、函数、结构体、类等
- 命名空间是开放的,支持随时往原先的命名空间追加内容
- 在无名 / 匿名命名空间中定义变量,相当于使用了
static
关键字定义变量,如static int age;
C++ 命名空间定义及使用语法:
- 命名空间定义的语法:
namespace name { … }
- 命名空间使用的语法:
using namespace name;
- 使用特定命名空间中的变量:
using name::variable;
- 使用默认命名空间中的变量:
::variable
- 默认情况下可以直接使用默认命名空间中的所有标识符
命名空间的编程实战一
学习目标
在 C++ 中,使用命名空间。
1 |
|
程序运行的输出结果如下:
1 | hello world |
命名空间的编程实战二
学习目标
在 C++ 中,使用 using 声明与 using 编译指令。
1 |
|
程序运行的输出结果如下:
1 | age = 20 |
C 语言和 C++ 的关系
C 语言是在实践的过程中逐步完善起来的,没有深思熟虑的设计过程,使用时存在很多 灰色地带
,残留了过多低级语言的特征,直接利用指针进行内存操作,其最终目标是程序执行效率的高效。当面向过程方法论暴露越来越多的缺陷的时候,业界开始考虑在工程项目中引入面向对象的设计方法,而第一个需要解决的问题就是:高效的面向对象语言,并且能够兼容已经存在的代码。C 语言和 C++ 语言的关系如下:
- C 语言和 C++ 并不是对立的竞争关系
- C++ 是 C 语言的加强,是一种更好的 C 语言
- C++ 是以 C 语言为基础的,并且完全兼容 C 语言的特性
- C 语言 + 面向对象方法论 = C++ / Objective C
C++ 对 C 语言的增强
实用性增强
C 语言中的变量都必须在作用域开始的位置定义,而 C++ 中更强调语言的 实用性
,所有的变量都可以在需要使用时再定义。
1 | int main(int argc, char *argv[]) |
函数检测增强
函数检测增强包括:参数类型检测、参数个数检测、返回值检测。C 语言默认数据类型在 C++ 编译器中是不合法的,C++ 中所有变量和函数必须声明类型。以下代码在 C 语言中可以编译通过,但在 C++ 中会编译报错。
1 | f(i) |
- 在 C 语言中,
int f()
表示返回值为int
,接受任意参数的函数 - 在 C 语言中,
int f(void)
表示返回值为int
的无参函数 - 在 C++ 中,
int f()
和int f(void)
具有相同的意义,都表示返回值为int
的无参函数 - C++ 更加强调类型,即任意的程序元素都必须显示声明类型
类型转换检测增强
1 | char *p = malloc(sizeof(64)); // C 语言编译器中编译不会报错,但是 C++ 编译器中会报错 |
1 | char *p = (char *) malloc(sizeof(64)); // 在 C++ 编译器中,需要强制转换类型,否则编译会报错 |
提示
值得一提的是,malloc()
函数的返回值是 void *
。
全局变量检测增强
在 C 语言中,重复定义多个同名的全局变量是合法的,但在 C++ 中,不允许定义多个同名的全局变量。在 C 语言中,多个同名的全局变量最终会被链接到全局数据区的同一个地址空间上。
1 | int g_var; |
1 | int g_var; |
struct 类型的增强
C 语言的 struct
定义了一组变量的集合,C 编译器并不认为这是一种新的类型,而在 C++ 中的 struct
是一个新类型的定义声明。值得一提的是,在 C++ 中结构体支持定义函数,而 C 语言则不支持。
1 |
|
const 关键字增强
提示
更多关于 const
关键字的详细使用教程,请看 这里。
- 在 C++ (而不是 C 语言)中,可以使用
const
值来声明数组的长度。 - 在 C 语言中,
const
修饰的变量是伪常量,编译器会分配内存,因此可以通过指针更改常量的值。 - 在 C++ 中,
const
修饰的变量是真常量,编译器一般不会分配内存(非绝对),而是使用符号表
进行处理,一般无法通过指针更改常量的值(非绝对)。 - 在 C 语言中,
const
默认是外部链接,而在 C++ 中const
默认是内部链接。若希望在其他 C++ 源文件(非当前源文件)中可以直接访问const
常量,则需要使用extern
关键字修饰常量,以此提高常量的作用域;使用extern
关键字后,编译器会给const
常量分配内存空间。
在 C 语言中,用
const
关键字在局部定义的常量,可以通过指针更改值,属于伪常量,代码如下:
1 |
|
程序运行的输出结果如下:
1 | *p = 30 |
在 C++ 中,用
const
关键字在局部定义的常量,一般情况下无法通过指针更改值,属于真常量,代码如下:
1 |
|
程序运行的输出结果如下:
1 | *p = 30 |
C++ 编译器会给 const 常量分配内存空间的几种情况
- (1) 对常量取地址时,编译器会临时开辟一块内存空间,指针指向的是这块临时内存空间,即无法通过指针真正改变常量的值。
- (2) 使用普通变量初始化常量时,编译器会分配内存空间,可以通过指针真正改变常量的值。
- (3) 使用
const
修饰自定义数据类型时(结构体、类),如const Person p;
,编译器会分配内存空间,可以通过指针真正改变常量的值。 - (4) 使用
extern
修饰const
常量时,编译器会分配内存空间。 - (5) 当使用字面量常量初始化
const
引用(即常量引用),如const int &a = 10;
,编译器会分配内存空间,可以通过指针真正改变常量引用的值。
在日常开发中,应该尽量使用 const
关键字替换 #define
预处理指令。在旧版本 C 语言中,如果想建立一个常量,必须使用 #define
预处理指令定义常量(也叫宏常量),如 #define MAX 1024
。我们定义的宏 MAX 从未被编译器看到过,因为在预处理阶段,所有的 MAX 已经被替换为了 1024,于是 MAX 没有将其加入到符号表中。当我们使用这个常量获得一个编译错误信息时,可能会带来一些困感,因为这个信息可能会提到 1024,但是没有提到 MAX。如果 MAX 被定义在一个不是你写的头文件中,你可能并不知道 1024 代表什么,也许解决这个问题要花费很长时间。为了避免这种问题的产生,可以用一个常量替换上面的宏定义,如 const int MAX = 1024
。
const 与 #define 的区别
const
有类型,可进行编译器类型安全检查;#define
无类型,不可以进行类型检查。const
有作用域,而#define
没有作用域的概念,有效范围是宏定义的位置到源文件的结尾。如果需要定义在指定作用域下才有效的常量,那么#define
就不能使用。const
常量可以拥有命名空间,而宏常量(使用#define
定义的常量)并没有命名空间的概念,它们是在预处理阶段进行简单的文本替换。宏常量在整个编译单元中是全局可见的,不受命名空间的限制。
register 关键字增强
register
是运行速度最快的关键字,其作用是请求编译器尽可能地将变量存在 CPU 内部的寄存器中,而不是通过内存寻址访问,以提高程序运行效率。注意这里是尽可能,不是绝对。首先,register
变量必须是能被 CPU 所接受的类型,这通常意味着 register
变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。C 语言中,register
关键字表示 “请求”(不一定成功)让变量直接放进寄存器中,方便访问,但是在 C 语言中不能取 register
变量的地址。C++ 对 register
进行了增强,C++ 编译器会对频繁被调用的变量主动申请为 register
,即使没有用 register
关键字声明,它也会这样做。值得一提的是,C++ 编译器当发现程序中需要对 register
变量取地址时,register
对变量的声明会变得无效。
1 | int main(int argc, char *argv[]) |
由于寄存器的数量有限,而且某些寄存器只能接收特定类型的数据(如指针和浮点数),因此真正起作用的 register
修饰符的数目和类型都依赖于实际运行程序的机器,而任何多余的 register
修饰符都将被编译器所忽略。在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度,这因为被占用的寄存器不能再用于其它用途;或者变量被使用的次数不够多,不足以抵消装入和存储变量所带来的额外开销。早期的 C 编译器不会自动把变量保存在寄存器中,除非程序员命令它这样做,这时 register
修饰符是 C 语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定哪些变量应该被存到寄存器中时,现代的 C 编译器能比程序员做出更好的决定。实际上,许多编译器都会忽略 register
修饰符,尽管它完全合法,但它仅仅是暗示而不是命令。
新增 bool 类型关键字
C++ 在 C 语言的基本类型系统之上增加了 bool
类型关键字,bool
可取的值只有 true
和 false
。理论上 bool
变量只占用一个字节,如果多个 bool
变量定义在一起,可能会各占一个 bit(位)
,这取决于编译器的实现。true
代表真值,编译器内部用 1
来表示,false
代表非真值,编译器内部用 0
来表示。C++ 编译器会在赋值时将非 0
值转换为 true
,0
值转换为 false
。
1 | int main(int argc, char *argv[]) |
程序运行的输出结果如下:
1 | b = 1, sizeof(b) = 1 |
三目运算符功能增强
- 使用三目运算符时,C 语言返回变量的值,C++ 是返回变量本身
- C 语言中的三目运算符返回的是变量值,不能作为左值使用
- C++ 中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方
- 特别注意:C++ 中三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用,例如
(a < b ? 1 : b )= 30;
- C 语言如何支持类似 C++ 的三目运算特性呢?当左值的条件:要有内存空间,而 C++ 编译器只是帮助程序员取了一个地址而已,C 语言版的写法为:
*(a < b ? &a : &b) = 30
1 | int main(int argc, char *argv[]) { |
程序运行的结果如下:
1 | a = 30, b = 20 |
双冒号作用域限定运算符
双冒号作用域限定运算符,用于对当前作用域之外的同名变量进行访问,例如在下面的例子中,可以利用 ::
实现在局部变量 a 的作用域内对全局变量 a 的访问。
1 |
|
程序运行的输出结果如下:
1 | local variable a = 3.14 |