本文基于 C++ 11,适合有 C 语言基础的阅读,只讲与C语言不同的地方。
如果需要了解最新式语法,那么可以去看微软 Visual Studio 2022 的文档《C++标准库参考(STL)》
C 和 C++ 的最主要的不同之处
C++ 是从 C 上改进的,主要新增了一点:
面向过程和面向对象的区别
- 面向过程是将问题按照步骤来编写代码;面向对象是将构成问题的事务分解成多个对象。
- 面向对象可以更好的处理规模更大,更复杂的问题。
- 不严格的来说,面向对象更像是把问题拆成一个个积木,这样遇到不同的问题,就可以将需要的积木拼接起来,而不用重写。而这里的积木就是对象。
例如一个绘画程序就可以分成:笔刷、画笔、橡皮、圆形、矩形等多个类。类的定义描述了每个类可以执行的操作,例如移动矩形或者旋转等操作。
那么什么东西使得 C++ 支持面向对象呢? 是类(class)。
面向对象程序设计有四个基本特点:抽象、封装、继承、多态:
- 抽象:将同一类事务的共同特点概括出来的过程,被称为抽象。抽象完得到的就是一个具体的个体,被称为“对象”。对象是系统中,用对象名、属性和操作来描述客观事物的实体。而多个对象的集合就是一个类。
- 封装:封装就是把对象的属性和操作结合成一个独立的单元。这样可以达到紧密地将数据和操作数据的函数联系起来;包含一部分属性和函数;将一些属性和函数对外开放,作为操作对象的接口。
- 继承:继承就是在写新的类的时候,以现有的类为基础。从而达到代码扩充和复用的目的。
- 多态:多态就是值不同的对象可以调用相同名称的函数,但是可以导致不同的行为。这被称为多态性。多态性导致开发者在使用的时候只用考虑如何使用,而不用考虑函数的使用细节。将细节留给了包含该函数的对象的开发者。这大大提高了人们解决复杂问题的能力。
而实现以上特点的基础就是类。
关于类的详细内容可以看这里:《C++ 中的类(class)和对象(object)详解》
C++ 程序结构
#include <iostream>
using namespace std;
int main()
{
int a[10];
for (int i=0; i<10; i++)
cin>>a[i];
for (int i=0; i<10; i++)
cout<<a[i]<<" ";
cout<<endl;
}
依次介绍一下各部分的含义:
- 开头的
#include <iostream> 和 using namespace std; ,一个表示头文件iostream ,输入输出流,不然没法使用 cin 和cout 。;另一个表示使用命名空间(这个下面会解释)。 cin 表示从输入。cout 表示输出。- 循环语句和 C 中差不多。
cout<<a[i]<<" "; 表示输出数组 a[i] 中的元素,然后在后面加两个空格。endl 表示换行符,效果和\n 一样。- 当连续从键盘读取数据的时候,以空格、制表符、换行符作为分隔符号。如果第一个字符就是分隔符号,那么
cin 会忽略并且清除掉。 - 自定义的数据类型不能直接使用
>> 和<< ,必须对其进行运算符重载。 return 0; 不是必须要写的。
命名空间
不同模块标识符之间可能会发生重名的情况,从而引发错误。C++中为了避免名称定义冲突,引入了“命名空间(namespace)”的定义,作用就是消除重名带来的歧义。
命名空间说白了就是可以自定义去包含哪些作用域,从而不会因为重名发生歧义,从而引发错误。
命名空间使用以下方式定义:
namespace 命名空间名称
{
...
}
使用方法如下:
using namespace 命名空间名称;
如果想要使用被嵌套的命名空间,那么需要使用两个冒号:: :
using namespace 父级命名空间名称::真正要使用的命名空间名称;
强制类型转换运算符
在 C 语言中,强制类型转换需要使用以下方式:
a = (double) b;
C++ 中也可以使用这一方式,但是还有很多其他的方式。所有可以使用的方式如下:
a = static_cast<double>(b);
a = double(b);
a = (double) b;
a = double b;
引用
在 C++ 中,引用会给一个变量起一个别名,类似某些语言中的alias 或者文件系统的软链接/符号链接。 引用的主要作用是用作函数的形参,直接使用原始数据而不是副本。就可以替代指针,使其可以为函数处理大型结构提供一种方便的途径。 此外还可以帮助不知道如何选择变量名的人。
方式如下:
int a=40;
int &b=a;
这个样式和 C 语言中传递指针地址的&a 一样,可以理解成把a 的地址赋值给了b 的地址。 需要注意的是,& 无论是靠着类型,还是变量名称,还是都不靠,都是可以的。
引用可以在变量、函数的返回值和参数中使用。
函数参数初始值和传递
可以在设置函数参数的时候初始化,从而达到默认值的效果。不过需要注意两点:
- 设置默认值要放在声明函数的部分。或者不用声明的话,可以写在定义部分。
- 不设置默认值的参数要放在前面,不能设置默认值的参数要放在后面。不能一个设置一个不设置。
设置函数参数和默认值的方式如下:
#include <iostream>
using namespace std;
int test(int a=1,int b=1);
int main()
{
int a = test(10);
cout<<a;
}
int test(int a,int b)
{
return a*b;
}
那么在通过参数传递值给函数的时候,有两种方式:
传递值需要注意的是这样只会改变形参(调用函数时,参数部分使用的变量本身并不会改变,不会对使用的变量本身的内存空间进行操作)。 传递引用的话是传递对象的首地址值(例如指向数组的指针其实是指向第一个元素的地址的),这样形参的改变就会进一步影响实参(因为调用函数的时候,一直是对地址进行操作,也就是直接对使用的变量本身的内存空间进行操作)。
下面来举个例子: 首先是直接传值,如下:
#include <iostream>
using namespace std;
void Swap(int a, int b)
{
int tmp;
tmp=a;
a=b;
b=tmp;
cout<<"在 Swap 函数中,\t\ta="<<a<<",b="<<b<<endl;
}
int main()
{
int a=10,b=20;
cout<<"数据交换前:\t\ta="<<a<<",b="<<b<<endl;
Swap(a,b);
cout<<"数据交换后:\t\ta="<<a<<",b="<<b<<endl;
}
输出结果:
数据交换前: a=10,b=20
在 Swap 函数中, a=20,b=10
数据交换后: a=10,b=20
可以看到,函数main 中的变量a 和b 的值并没有变。
但是使用引用之后呢?
#include <iostream>
using namespace std;
void Swap(int& a, int& b)
{
int tmp;
tmp=a;
a=b;
b=tmp;
cout<<"在 Swap 函数中,\t\ta="<<a<<",b="<<b<<endl;
}
int main()
{
int a=10,b=20;
cout<<"数据交换前:\t\ta="<<a<<",b="<<b<<endl;
Swap(a,b);
cout<<"数据交换后:\t\ta="<<a<<",b="<<b<<endl;
}
数据交换前: a=10,b=20
在 Swap 函数中, a=20,b=10
数据交换后: a=20,b=10
可以发现,函数main 中的变量a 和b 的值被交换了。
内联函数(inline)
C++ 新增了内联函数,用于替代#define 来定义小型函数。这是一项为了提高程序运行速度的改进。与常规函数相比,主要区别在于编译器如何编译。
首先需要知道,编译器在编译代码的时候,会将代码编译成可执行程序,也就是一组机器指令。运行程序的时候,操作系统将可执行程序(也就是机器指令)载入内存中,每条指令都有自己的地址。机器指令随后按照地址逐步执行这些指令。但是有些指令是会向前或向后跳转一些指令的(例如循环或者选择语句),跳转到指定地址继续执行。
调用常规函数的时候,程序会跳转到函数的地址,然后执行,在函数结束时返回。 但是内联函数就不用跳转。因为编译器编译的时候直接就将其代码替换函数名,编译后的指令就在后面,于是不用跳转。这提高了运行速度,代价是需要占用更多的内存。
下面时典型实现的介绍
执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并且函数参数复制到堆栈中(为此保存的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处。来回跳跃需要一定的开销。
例如下面两段代码是等价的(指的是编译之后的可执行文件,而不是效果):
#include <iostream>
using namespace std;
inline int square(int a)
{
return a*a;
}
int main()
{
int a=3;
cout<<square(a);
}
#include <iostream>
using namespace std;
int main()
{
int a=3;
cout<<a*a;
}
内联函数和宏的区别
内联函数其实就是 C 语言中宏(使用#define 提供)的升级版(宏是内联函数的原始实现)。
宏是使用文本替换的方式来实现的,而不是参数传递。 举个例子,定义一个计算平方的宏:
#define SQUARE(X) X*X
在编译的时候,会使用括号内的内容(也就是X 部分的内容)来替换后面公式X 的部分。X 被称为“参数”的符号标记。这导致会有一些需要注意的地方,如下:
a=SQUARE(2);
b=SQUARE(13+14)
a 的值是正确的,是4 ;但是b 相当于13+14*13+14 .这是因为直接是文本替换,而不是引用参数导致的。要想正常工作只能进行改进,如下:
#define SQUARE(X) ((X)*(X))
但是这样还有问题,比如使用i++ 这种,会导致式子变成(i++)*(i++) ,会让i 自增两次。
所以 C++ 改进了这些问题,于是出现了内联函数。
const 和指针
const 用于在声明变量的时候,将其设置为一个常量,也就是不能更改值的变量。一般样式为const int a = 0; 。 除了在最左侧的时候,修饰左侧的内容。
但是在与指针使用的时候,const 的位置会决定使用效果。关于指针的说明可以看这里:《C/C++ 指针小笔记》
首先是和普通变量一样,放在开头:const int *a = b; 。这样表示指针a* 指向的是一个常量。因此无法通过指针*a 来修改b 的值。 其次可以这样:int *const a = b; 。这样表示指针a 是一个常量。因此只指向b ,无法更改指向。 还有一种就是const int *const a = b; 或者int const *const a = b; 。这种就是既不能通过指针来修改,也不能修改指向。
bool 数据类型
相比较 C 语言,C++ 新增了bool 数据类型。 该数据类型只有两个值,1 或 0。1 表示真,0 表示假。
string 数据类型
相比较 C 语言,C++ 新增了string 数据类型,用于处理字符串。不过string 数据类型其实就是 C 语言的改进版:string 数据类型存储的是字符串的首地址(和数组一样),而非字符串本身。
声明和初始化
声明和初始化方式如下:
string str;
string str2="Hello";
当然string 数据类型也兼容 C 语言的字符数组。如下:
char strc[]="World!";
string str="Hello";
string str2=strc;
cout<<str<<", "<<str2;
输出的结果如下:
Hello, World!
字符串数组
像其他数据类型一样,可以声明string 数据类型的数组。方法如下:
string strArr[]={"Hello", ", ", "World", "!"};
字符串比较
字符串也可以通过运算符进行比较。大小的判定是通过字典顺序(而不是字符编码大小),字符串长短(ab 比 a 大),并且大小写相关(小写字母比大写字母大)。 可以做个简单的测试来感知一下。在 ASCII 编码中,小写字母 a-z 是在大写字母 A-Z 之前的。那么就输入以下代码来比较A 和h :
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str1="A";
string str2="h";
int a=str1>str2;
cout<<a;
}
输出结果为:
0
这里的比较会和 C 语言中一样,如果为成立,返回一个1 ,如果不成立,则返回0 。
这里就可以发现,比较下来,A 小于h ,符合字典顺序。
字符串拼接
可以使用运算符+ 来连接字符串。
如果只是为了简单地输出,那么可以使用如下方法:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str1="ab";
string str2="a";
cout<<str1+str2;
}
输出:
aba
如果是为了处理字符串,那么可以使用string.h 库中的strcat() ,如下:
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char dest[50] = "Learning C++ is fun";
char src[50] = " and easy";
strcat(dest, src);
cout << dest ;
return 0;
}
需要注意的是,这个方法有些 C++ 编译器不支持 string 类型,需要使用字符数组。
其他的一些操作
C++ 支持一些字符串操作,下面是一些常用的。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
string str1="abcdefcd";
string str2="efghijklmn";
cout<<"字符串为:\t\t\t"<<str1<<endl;
cout<<"字符串长度为:\t\t\t"<<str1.size()<<endl;
cout<<"字符串加上'abc':\t\t"<<str1.append("abc")<<endl;
cout<<"裁切字符串前 3 个元素:\t\t"<<str1.substr(0,3)<<endl;
cout<<"从第5个字符开始查找'c'的位置:\t"<<str1.find("e",4)<<" (从 0 开始)"<<endl;
cout<<"返回第5个字符串:\t\t"<<str1.at(4)<<endl;
}
输出:
字符串为: abcdefcd
字符串长度为: 8
字符串加上'abc': abcdefcdabc
裁切字符串前 3 个元素: abc
从第5个字符开始查找'c'的位置: 4 (从 0 开始)
返回第5个字符串: e
如果想要找到更多,还请查看微软提供的文档,这里比较全:basic_string Class
头文件
C++ 中,头文件从.h 后缀改成了.hpp 后缀,而且还可以继续使用前者。使用方法没啥区别。
如下有一个hpp 文件和一个cpp 文件:
class Point
{
private:
public:
int x, y;
int getX();
};
int Point::getX()
{
return x;
}
#include <iostream>
#include "custom.hpp"
using namespace std;
int main()
{
Point a;
a.x=10;
a.y=20;
cout<<"x = "<<a.getX();
return 0;
}
编译后得到输出为:
x = 10
标识符的作用域
C++ 中,标识符的作用域有:
- 函数原型作用域
- 局部作用域(块作用域)
- 类作用域
- 命名空间作用域
下面介绍一下:函数原型作用域和(其他的在 C 中都很常见)。 声明函数原型时,形参的作用范围就是函数原型作用域。是 C++ 中最小的作用域。
int copy(string str1, string str2);
参数的作用范围就是左右圆括号之间,这部分就是函数原型作用域。
数组
C 和 C++ 中都支持一维数组和多维数组。指针和数组的套用这里不讨论。
一维数组
这个和大部分语言一样,声明格式如下:
类型 数组名[数组长度];
定义格式如下:
数组名[数组长度]={元素1, 元素2...};
需要注意一点,如果在声明的时候就定义或者初始化,那么可以不用写数组长度。如下:
int a[]={1,2,3,4};
int a[];
a[]={1,2,3,4};
多维数组
实际开发中,指针数组比多维数组使用的多。
多维数组必须在声明的时候初始化(也就是定义)。方式如下:
int a[2][12]={{1,2,3}, {4,5,6}};
数组名称后面的第一个方括号[] 里表示有几维度的数组(可以忽略),第二个方括号[] 后面的和一维数组的一样,表示最大元素数。 每一维度的数组使用大括号{} 包裹起来,并且使用逗号, 隔开。
动态分配内存空间和释放
在 C++ 中,使用new 和delete 来进行动态分配内存空间和释放。 对于一般数据类型,动态内存空间和释放方式如下:
int a;
a=new int;
delete a;
对于数组来说,有一些小变化,如下:
int *a;
a=new int[2];
delete []a;
等价于
int *a=new int[2];
delete []a;
运算符重载
简介
运算符的实质是编写以运算符 为名称的函数,使用运算符的表达式就是调用一个重载函数。
返回值类型 operator运算符(参数)
{
函数体
}
- 运算符可以被重载为全局函数(通常为类的友元函数,因为全局函数不能访问类的私有成员),对于二元运算符,需要传递两个参数。
- 运算符可以被重载为类的成员函数,对于二元运算符,只需传递一个参数。
不可以被重载的运算符
. :成员访问运算符->* :成员指针访问运算符:: :域运算符sizeof :长度运算符?: :三元(条件)运算符# :预处理符号
重载运算符的规则
- 重载后的运算符的含义应该符合原有的用法习惯;
- 运算符重载不能改变运算符原有的语义,包括运算符的优先级和结合性;
- 运算符重载不能改变运算符操作符和操作数的个数及语法结构;
- 不能创建新的运算符,也就是说不能超出 C++ 允许重载的运算符范围;
- 重载运算符
() 、[] 、-> 或赋值运算符= 时,只能将它们重载为成员函数,不能重载为全局函数; - 运算符重载不能改变该运算符作用于基本数据类型对象的含义。
重载赋值运算符
重载赋值运算符= 时,只能将它们重载为成员函数,不能重载为全局函数。系统默认将其重载为对象成员变量的复制。
同类对象之间可以通过赋值运算符= 互相赋值。如果没有经过重载,那么= 的作用就是将右侧的对象的值一一赋值给左侧的对象。这相当于值的拷贝,被称为“浅拷贝”。 浅拷贝可能会造成:
- 重复释放同一块空间,进而产生错误。比如对象中有一个指针成员变量,这个指针指向的空间会被释放两次,就会报错。
- 某块内存永远不会被释放,而成为内存垃圾。比如一个对象声明的时候调用构造函数初始化了,然后将其等于另外一个对象,那么这个对象原本初始化时的那部分空间就会称为内存垃圾。
重载赋值运算符之后,赋值语句的功能是将一个对象中的指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方,这样的拷贝被称为“深拷贝”。
重载赋值函数的声明和定义方式如下:
返回值类型 &operator=(参数);
返回值类型 &类名::operator=(参数)
{
函数体
return 返回值;
}
这里会发现operator= 函数使用了引用,这是为了让重载后的赋值运算符仍然可以连续使用,例如a=b=c 。(如果不使用引用,没办法更改原本的值)
重载流插入运算符和重载流提取运算符
在 C++ 语言中,最常被重载的是<< 和>> 运算符。最早 C 和 C++ 中,<< 是位左移,>> 是位右移。但是在ostream 对<< 、ostream 对>> 进行了重载,让其重载成流插入运算符和流提取运算符。这两个类包含在头文件iostream 中,而cout 和cin 则是它们的对象。
重载流插入运算符和重载流提取运算符的目的是为了方便输出,不然就需要在类中写一个成员函数来进行输出。
由于二者格式比较像,所以只写重载流插入运算符的格式,重载<< 也是最常见的。
ostream& operator<<(ostream& os, const myDate & a)
{
return os;
}
需要注意几点:
- 在类中声明为友元函数;
- 函数返回值类型为
ostream& ,不能省略引用,不然返回值类型不同不能重载。
下面举个例子
#include <iostream>
using namespace std;
class myDate
{
private:
int year, month, day;
public:
myDate(int, int, int);
int returnDate();
friend ostream& operator<<(ostream& os, const myDate & a);
};
myDate::myDate(int y, int m, int d)
{
year=y;
month=m;
day=d;
}
int myDate::returnDate()
{
cout<<year<<"/"<<month<<"/"<<day<<endl;
return 0;
}
ostream& operator<<(ostream& output, const myDate & a)
{
output<<a.year<<"/"<<a.month<<"/"<<a.day<<endl;
return output;
}
如果没有重载<< 的话,那么就得调用成员函数returnDate() 。
输入/输出流
C 和 C++ 并不像一些语言把输出和输出建立在语言中,而是留给了编译器开发者,这样可以让编译器开发者自由设计 I/O 函数,从而适合目标计算机的硬件要求。实际上,多数开发者都把 I/O 建立在最早为了 UNIX 环境开发的库函数基础上,也就是 C 语言的stdio.h 中的 C 函数,但是 C++ 依赖于自己的 I/O 解决方案(在iostream 和fstream ),而不是 C 的。
在 C++ 的 STL 中,将与 I/O 相关的类统称为流类。
下面流类都包含在同名文件中:
- ios:流的基类(其实还有一个
ios_base 类,包含了流的基本特征,如是否可以读取、二进制流还是文本流,而ios 则是基于ios_base 的)。 - istream:通用输入流基类和其他输入流基类,是
ios 的派生类。cin 就是该类的对象。 - ifstream:文件输入流类。用于从文件读取数据。
- ostream:通用输出流基类和其他输出流基类,是
ios 的派生类。cout 就是该类的对象。 - ofstream:文件输出流类。用于向文件写入数据。
- iostream:通用输入/输出流基类和其他输入/输出流基类。
- fstream:文件输入/输出流类。用于向文件读取/写入数据。
控制 I/O 格式
C++ 进行 I/O 格式控制的方式一般有流操作符、设置标志字和调用成员函数。
流操作符
流操作符有很多很多,这里不细说,就讲一下用法。详细的可以看这里: iomanip
下面是将一个整数以十六进制输出:
int main() {
cout<<hex<<65535;
return 0;
}
结果为:
ffff
我们还可以使用setbase 来实现同样的效果:
int main() {
cout<<setbase(16)<<65535;
return 0;
}
而且还可以通过showcase 来加上前缀:
int main() {
cout<<showbase<<setbase(16)<<65535;
return 0;
}
结果为:
0xffff
标志字
头文件iostream 中setiosflags() 函数用于指定标志,函数的参数为流动格式标志位。(Visual Studio 2022 中,setiosflags() 函数还是在头文件iomanip 中)
函数模版
函数模版的定义格式大致如下:
template <类型 参数名, 类型 参数名...>
返回值类型 函数模板名(参数)
{
}
关于第一行的类型 和参数名 有以下两点说明:
类型 可以使用class 或typename 标识符(前者一般是 C++98 之前使用),指明函数模板中可以接收的类型参数,参数名由标识符表示。参数名 将这个类型命名,必须是常量,用于后面自动替换类型。
下面是一个例子:
template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp=a;
a=b;
b=temp;
}
类模版
类模版和函数模板几乎一样,不同的是类还有需要声明。
类模板的成员函数可以在类体内进行说明,自动成为内联函数。但是如果在类模版以外去定义成员函数的话,那么就需要采用以下格式:
template <模板参数表>
返回类型 类模板名<模板参数标识符列表>::成员函数(参数表)
{
}
下面举个例子:
template <class T1, class T2>
class Pair
{
public:
T1 first;
T2 second;
Pair(T1 k, T2 v):first(k),second(v){}
};
|