课程简介

课程链接:C++教程 | c3程序员

老师主页:C3程序猿

参考教材:c++ primer plus by Stephen Prata

C++官方文档:C++ reference

VSCode环境搭建

C/C++ for Visual Studio Code

在VSCode搭建C/C++环境

  • 下载MinGW安装并配置根目录下的bin文件夹路径D:\MinGW\bin到环境变量

    1
    g++ --verison //用于检查是否安装配置成功
  • VSCode安装插件

    • Code Runner
    • C/C++
  • File>Preference>Settings>搜索

    • Run In Terminal: √Code-runner: Run In Terminal
    • Save File Before Run: √Code-runner: Save File Before Run
    • Auto Guess Encoding: √Files: Auto Guess Encoding
  • 中文乱码

  • 测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream>  //header files
    using namespace std;

    int main() //function header
    {
    system("chcp 65001");
    cout<< "Hello, 世界!";
    return 0;
    }

Use VS Code snippets to generate main()

File>Preference>Configure User Snippets>cpp.json(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
// Place your snippets for cpp here. Each snippet is defined under a snippet name and has a prefix, body and
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
// same ids are connected.
// Example:
"Print to console": {
"prefix": "snippet_main",
"body": [
"#include <iostream>",
"using namespace std;",
"int main()",
"{",
" ",
" return 0;",
"}",
],
"description": "framework of main() for cpp"
}
}

Type in template_main (followed with shortcut Ctrl+I or Ctrl+Space to trigger focusSuggestion) then press Enter or Tab to make it work.

一段简单程序的输出&注释

  • hello world.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    #include <iostream>  //header files
    using namespace std;

    int main() //function header
    {
    cout<< "Hello World!";
    return 0;
    }
    1
    2
    3
    system("pause");
    //有些编译器(如VisualStudio)可能在运行程序后会自动关闭终端
    //在return前使用这个命令可以阻止其立即关闭终端
  • 注释快捷键
    VS Code:Ctrl+/

  • 缩进快捷键

    VS Code:Ctrl+{/}

main 函数

mian函数的一些特点

  • 程序入口,由操作系统调用
  • 一个项目要有且只能有一个main函数

mian函数的一些形式

  • c++标准形式

    1
    2
    3
    4
    int main()
    {
    return 0;
    }
  • c标准形式

    1
    2
    3
    4
    int main(void)
    {
    return 0;
    }
  • 一些其它形式

    1
    2
    3
    4
    5
    int main(int argc, char* argv[])
    {
    return 0;
    }
    // 命令行参数(c/c++)
    1
    2
    3
    4
    5
    main(void)
    {
    return 0;
    }
    // 旧标准c允许,c++不允许
    1
    2
    3
    4
    5
    void main()
    {
    return 0;
    }
    // 逻辑上符合,很多系统支持,少部分系统不支持(不提倡)

main函数传参

1
2
3
4
5
6
7
8
9
10
// test.c
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("argc = %d\n", argc);
for(int i=0; i<argc; i++){
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
// test.cpp
#include <iostream>
int main(int argc, char *argv[])
{
std::cout << "argc = " << argc << std::endl;
for(int i=0; i<argc; i++){
std::cout << "argc["<<i<<"] = " << argv[i] << std::endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
~$ ./test
argc = 1
argv[0] = ./test
~$ ./test s1 s2 s3
argc = 4
argv[0] = ./test
argv[1] = s1
argv[2] = s2
argv[3] = s3

头文件

  • c++标准写法

    1
    2
    3
    4
    5
    #include <iostream>
    using namespace std;
    //i--input
    //o--output
    //stream -- information stream
  • c语言的标准写法(c++也支持)

    1
    2
    #include <iostream.h>
    //c语言标准写法,c++也支持这种.h写法

cout函数

  • 基本用法

    1
    2
    3
    cout << "Hello World!";
    cout << "I am " << 18 << "years old.";
    //连续输出,自动识别数据类型(printf需要指定数据类型)
  • 注意点
    cout 是个对象,不是关键字,也不是函数

endl&’\n’

  • 示范

    1
    2
    cout << "Hello World!" << endl;  //换行并清空缓冲区
    cout << "Hello World!" << '\n'; //换行
  • endl比’\n’多了一个刷新缓冲区的操作,这个操作会使缓冲区的字符立刻显示到屏幕上,’\n’则不保证这一点。

  • 理论上,’\n’显示会比endl慢半拍,但实际使用中,两者基本没有区别。

cin函数

  • 基本用法

    1
    2
    3
    4
    5
    int a,b,c;
    cout << "Please input 3 integers: ";
    cin >> a >> b >> c;
    cout << a << b << c;
    // cin可以自动识别变量类型,比scanf更加智能(需要指定输入类型%d%c%s...)
  • 注意点
    cin也是个对象,不是关键字,也不是函数

数据类型

数字类型

int 类型为4个字节,32比特,存储范围[231,231)[-2^{31},2^{31}),即 -2147483648 到 2147483647。

1
cout << sizeof(int) << " bytes"; // 4 bytes

常量类型

常量类型为只读类型的数据结构,其初始化后不能再被修改

1
2
3
const int ZERO = 0;
ZERO = 1;
// error: assignment of read-only variable 'ZERO'

位运算

与: &, 或: |, 非: ~, 异或: ^

数组

数组的初始化

  • 初始化列表

    1
    int intArray[3] = {1, 2, 3};
  • 等号可以省略

    1
    int intArray[3] {1, 2, 3};
  • 对部分元素初始化,其余默认为0

    1
    int intArray[3] {1}; // {1, 0, 0}
  • 初始化为0

    1
    int intArray[3] {0}; // {0, 0, 0}
  • 初始化列表为空,默认所有元素初始化为0

    1
    int intArray[3] {}; // {0, 0, 0}
  • 不指定数组元素个数,由初始化列表决定

    1
    int intArray[] {1, 2, 3};

char 数组

std::cout方法对字符数组的输出有特殊处理(重载),所以使用cout输出字符数组得到的是字符数组名的内容,而使用cout输出普通数组名得到的是数组的首地址。

1
2
3
4
5
6
7
char charGroup[] = {'a','b','c'};
cout << charGroup << endl;
// abc

int intGroup[] = {1, 2, 3};
cout << intGroup << endl;
// 0xcb99dffbc0

数组名arr、数组名取地址&arr、数组首地址&arr[0]

  • 数组名arr
    数组名可以作为数组第一个元素的指针。由数组和指针的关系知道,arr代表这个地址的十六进制数值,它相当于一个指针,指向第一个元素(&arr[0]),即指向数组的首地址。数组中的其他元素可以通过arr的位移(指针算术)得到,即 arr+i == &a[i]。

  • 数组名取地址&arr
    对于一个普通的变量a,&a是指用取地址符号取得变量a的内存地址;但是对于数组,arr在内存中并没有分配空间,只对数组arr的各个元素分配了存储空间,此处数组名字arr显然不是普通的变量,&arr也不代表所取arr的存储地址,它其实是数组的首地址,sizeof(arr)表示的也是整个数组的字节数。

  • 数组首地址&arr[0]
    这个就是取地址的最直接的应用,相当于对普通变量取地址。arr[0]在内存中实际分类存储空间,而&arr[0]就是取该存储空间的地址。同样对于任意满足范围的i,&a[i]就是取第i个元素的存储地址。

  • 动态数组指针

    对于一个指向动态数组的指针pArr,&pArr表示的是指针本身的地址,而pArr存储的是new出的动态数组的首地址,sizeof(pArr)表示的也是指针的字节数而非整个数组的字节数。并且通过new方法创建的动态数组是不能知道它的长度(大小/字节数)的,除非在创建时另外存储它的长度。

    1
    int *pArr = new int[n];

二维数组

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int twoDim[2][3] = {{1,2,3}, {4,5,6}};
    for (int row=0; row<2; row++)
    {
    for(int col=0; col<3; col++)
    cout << twoDim[row][col] << " ";
    cout << endl;
    }
    // 1 2 3
    // 4 5 6
  • 使用指针访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int twoDim[2][3] = {{1,2,3}, {4,5,6}};
    for (int row=0; row<2; row++)
    {
    for(int col=0; col<3; col++)
    cout << *(*(twoDim + row) + col) << " ";
    cout << endl;
    }
    // 1 2 3
    // 4 5 6
  • 二维数组在内存中连续存储
    从下面程序的输出结果可以看到二维数组的元素在空间中的存储地址是连续的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int twoDim[2][3] = {{1,2,3}, {4,5,6}};
    for (int row=0; row<2; row++)
    {
    for(int col=0; col<3; col++)
    cout << &twoDim[row][col] << " ";
    cout << endl;
    }
    // 0x9ba9fff8a0 0x9ba9fff8a4 0x9ba9fff8a8
    // 0x9ba9fff8ac 0x9ba9fff8b0 0x9ba9fff8b4
  • 二维可以看成一个元素类型为数组的一维数组
    上面示例中的 twoDim 由2个整型数组构成,twoDim[0]twoDim[1] 分别为它的第一个元素和第二个元素,它们的大小分别都是 12=sizeof(int[3])

    • twoDim的指针算术±1将在地址上相差12=sizeof(int[3])个字节,即twoDim+1twoDim相差12个bytes。
    • twoDim[0]twoDim[1]就是普通的一维数组,它们的指针算术±1将相差4=sizeof(int)个字节,即twoDim[0]+1twoDim[0]相差4个bytes。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int twoDim[2][3] = {{1,2,3}, {4,5,6}};

    cout << "sizeof(twoDim): " << " " << sizeof(twoDim) << endl;
    cout << "twoDim: " << twoDim << "\t" << "twoDim+1: " << twoDim+1 << endl;
    // sizeof(twoDim): 24
    // twoDim: 0xa4bdbff680 twoDim+1: 0xa4bdbff68c
    // (+12)

    cout << "sizeof(twoDim[0]): " << " " << sizeof(twoDim[0]) << endl;
    cout << "twoDim[0]: " << twoDim[0] << "\t" << "twoDim[0]+1: " << twoDim[0]+1 << endl;
    // sizeof(twoDim[0]): 12
    // twoDim[0]: 0xa4bdbff680 twoDim[0]+1: 0xa4bdbff684
    // (+4)

    cout << "sizeof(twoDim[1]): " << " " << sizeof(twoDim[1]) << endl;
    cout << "twoDim[1]: " << twoDim[1] << "\t" << "twoDim[1]+1: " << twoDim[1]+1 << endl;
    // sizeof(twoDim[1]): 12
    // twoDim[1]: 0xa4bdbff68c twoDim[1]+1: 0xa4bdbff690
    // (+4)
  • 二维数组→一维数组
    由于二维数组在内存中的地址是连续的,所以我们可以把二维数组看成一个一维数组,其中twoDim[0]或者*twoDim是这个一维数组的首地址。
    注意是twoDim[0]或者*twoDim,不是twoDim。虽然它们cout出的结果都是同一个十六进制地址,但是它们的指针算术不一样,twoDim+1将在地址上位移12个字节,*twoDim+1将在地址上位移4个字节。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int twoDim[2][3] = {{1,2,3}, {4,5,6}};

    for (int idx=0; idx<2*3; idx++)
    cout << twoDim[0][idx] << " ";
    // 1 2 3 4 5 6

    cout << endl;
    for (int idx=0; idx<2*3; idx++)
    cout << *(*twoDim + idx) << " ";
    // 1 2 3 4 5 6
  • 使用迭代器遍历二维数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    vector<vector<int>> vec = {{1,10}, {2, 20}, {3, 30}};
    for(auto iter1=vec.begin(); iter1!=vec.end(); iter1++){
    for(auto iter2=(*iter1).begin(); iter2!=(*iter1).end(); iter2++){
    cout << *iter2 << " ";
    }
    cout << endl;
    }
    // 1 10
    // 2 20
    // 3 30
    1
    2
    3
    4
    5
    6
    7
    8
    vector<vector<int>> vec = {{1,10}, {2, 20}, {3, 30}};
    for(auto iter=vec.begin(); iter!=vec.end(); iter++){
    for(auto num : *iter) cout << num << " ";
    cout << endl;
    }
    // 1 10
    // 2 20
    // 3 30
    1
    2
    3
    4
    5
    6
    7
    vector<vector<int>> vec = {{1,10}, {2, 20}, {3, 30}};
    for(auto iter=vec.begin(); iter!=vec.end(); iter++){
    cout << (*iter)[0] << " " << (*iter)[1] << endl;
    }
    // 1 10
    // 2 20
    // 3 30
    1
    2
    3
    4
    5
    6
    7
    8
    vector<vector<int>> vec = {{1,10}, {2, 20}, {3, 30}};
    for(auto iter=vec.begin(); iter!=vec.end(); iter++){
    vector<int> vec1 = *iter;
    cout << vec1[0] << " " << vec1[1] << endl;
    }
    // 1 10
    // 2 20
    // 3 30
    1
    2
    3
    4
    5
    6
    7
    vector<vector<int>> vec = {{1,15}, {2, 20}, {3, 10}};
    for(vector<int> vec1 : vec){
    cout << vec1[0] << " " << vec1[1] << endl;
    }
    // 1 10
    // 2 20
    // 3 30
  • 高维数组
    和二维数组同样的思路,在面对n维数组时,可以将n维数组看成一个元素是n-1维数组的一维数组。例如将3维数组看成一个元素是二维数组的一维数组。

动态二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int row = 2, col = 3;
// 先创建一个指针的指针,指向一个指针数组
int **twoDim = new int* [row];

// 循环new一组空间,将其首地址赋值给指针数组
for (int i = 0; i < row; i++)
*(twoDim + i) = new int[col];

// 输出动态数组元素的地址,注意不同行元素的地址不连续
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
cout << *(twoDim + i) + j << " ";
cout << endl;
}
// 0x1ad45307ec0 0x1ad45307ec4 0x1ad45307ec8
// 0x1ad45307f10 0x1ad45307f14 0x1ad45307f18

关于数组越界

C++不会对数组越界行为进行检查。(检查数组越界,编译器就必须在生成的目标代码中加入额外的代码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降)

不检查下标也为程序员提供了更大的操作空间,为指针操作带来更多的方便。C的数组标识符,里面并没有包含该数组长度的信息,只包含地址信息,所以语言本身无法检查,只能通过编译器检查,而早期的C语言编译器也不对数组越界进行检查,只能由程序员自己检查确保。

如果数组下标越界了,那么它会自动接着那块内存往后写。如果界外的空间暂时没有被利用,那么我们可以占用那块内存,但是如果界外的内存已经存放了东西,那么我们越界过去就会覆盖那块内存,则有可能会导致错误或是程序最终的运行结果出错,这些结果都是未知的。

字符串

C风格字符串

字符串的初始化

  • c风格字符串,结尾空字符'\0'

    1
    2
    char cString[] = {'a', 'b', 'c', '\0'};
    cout << cString << endl; // abc
  • 使用字符串常量 String Constant (引号引起的字符串"...") 进行初始化

    1
    2
    char cString[] = "abc";
    cout << cString << endl; // abc

字符常量&字符串常量

  • 单引号引起的字符为字符常量,例如 'a', 'A', '\0'
  • 双引号引起的字符为字符串常量,例如 "a", "cat", "zero"

其中 "a" = {'a', '\0'},这是两者的区别

拼接字符串

有时,字符串可能太长,无法方便地放在一行代码中。c++允许您连接字符串——也就是说,将两个带引号的字符串组合成一个字符串。

1
2
3
char cString[] = "Sometimes a string may be too long to conveniently fit on one line of code."
" C++ enables you to concatenate string literals--that is, to combine two quoted strings into one.";
cout << cString << endl;

strlen()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <cstring>
using namespace std;

int main()
{
char cString[] = "abc";
cout << cString << endl; // abc
// strlen输出字符串的长度,不是数组的长度,不包含'\0'
cout << strlen(cString) << endl; // 3
// sizeof计算整个字符串数组的大小,包含'\0'
cout << sizeof(cString) << endl; // 4 (bytes)

return 0;
}

字符串的读取

  • 直接使用 cin <<, 它会检测空白(空格,制表符,换行符)作为字符串结束标识,这样的会导致键入的字符串不能包含空格

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const int Size = 20;
    char name[Size]; char school[Size];

    cout << "Input your name: "; cin >> name;
    cout << "Input your School: "; cin >> school;
    cout << "\nName:" << name << "\tSchool: " << school;

    // Input your name: zhang wx
    // Input your School:
    // Name:zhang School: wx
  • 使用 cin.getline() 可以解决这个问题,它只检测换行作为字符串结束标识

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const int Size = 20;
    char name[Size]; char school[Size];

    cout << "Input your name: "; cin.getline(name, Size);
    cout << "Input your School: "; cin.getline(school, Size);
    cout << "Name:" << name << "\tSchool: " << school;

    // Input your name: zhang wx
    // Input your School: whut
    // Name:zhang wx School: whut
  • 使用 cin.get() 读取输入字符串时需要注意换行符
    cin.get() 的使用和 cin.getline() 类似,但是 get() 不是读取并丢弃换行符,而是将换行符留在输入队列中。如果连续两次调用 cin.get(),那么第二次 cin.get() 将会自动读取第一次输入结束的换行符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const int Size = 20;
    char name[Size]; char school[Size];

    cout << "Input your name: "; cin.get(name, Size);
    cout << "Input your School: "; cin.get(school, Size); // 第二次自动读取了换行符

    cout << "Name:" << name << "\tSchool: " << school;
    // Input your name: zhang wx
    // Input your School: Name:zhang wx School:

    使用不带参数的 cin.get() 可以处理这个问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const int Size = 20;
    char name[Size]; char school[Size];

    cout << "Input your name: "; cin.get(name, Size);
    cin.get(); // 处理输出队列中的换行符
    cout << "Input your School: "; cin.get(school, Size);

    cout << "Name:" << name << "\tSchool: " << school;
    // Input your name: zhang wx
    // Input your School: whut
    // Name:zhang wx School: whut

    其中

    1
    cin.get(name, Size); cin.get();

    可以连起来使用:

    1
    cin.get(name, Size).get();
  • 为什么要使用get(),而不是getline()呢?
    首先,老式实现没有getline()。其次,get()使输入更仔细。例如,假设用get()将一行读入数组中。如何知道停止读取的原因是由于已经读取了整行,而不是由于数组已填满呢?查看下一个get()的输入字符,如果是换行符,说明已读取了整行;否则,说明该行中还有其他输入。
    总之,getline()使用起来简单一些,但get()使得检查错误更简单些。可以用其中的任何一个来读取一行输入;只是应该知道,它们的行为稍有不同。

  • 数字字符混合输入需要注意换行符

    1
    2
    3
    4
    5
    6
    7
    8
    const int Size = 20;
    int age; char name[Size];

    cout << "Input your age: "; cin >> age;
    cin.get(); // 处理输出队列中的换行符
    cout << "Input your name: "; cin.get(name, Size);

    cout << "Age: " << age << "\tName:" << name;

    其中

    1
    cin >> age; cin.get();

    可以连起来使用:

    1
    (cin >> age).get();
  • 使用 std::getline() 可以读取带空格的输入并写入 string 类型的变量, 可以不使用 char[] 数组来表示字符串.

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>
using namespace std;

int main() {
string str;
getline(cin, str);
cout << str << endl;
return 0;
}

string 类型

使用 string 类型需要在头文件中包含<string> 或者使用命名空间 std

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main()
{
string str;
cout << "Input your name: "; cin >> str;
cout << "Your name: " << str;
}
// Input your name: zhang wx
// Your name: zhang

直接使用 cin >> 读取输入字符串会以空字符作为输入结束标识,使用 getlin(cin, str) 读取字符串输入可以避免

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

int main()
{
string name, school;
cout << "Input your name: "; getline(cin, name);
cout << "Input your school: "; getline(cin, school);
cout << "Your name: " << name << "\tSchool: " << school;
}
// Input your name: zhang wx
// Input your school: whut
// Your name: zhang wx School: whut

string类的一些方法

1
2
3
4
5
6
7
8
string str;
str.push_back('a');
str.pop_back();
str.length();
if(str.empty()) cout << "true" << endl;

string str1="a", str2="b";
str.swap(str2);

const char* 和 string

1
2
3
4
5
string str = "helloworld"; // works
const string const_string = "helloworld"; // works
string & string_ref= "helloworld"; // doesn't work
// a reference of type "std::string &" (not const-qualified) cannot be initialized with a value of type "const char
const string & const_string_ref = "helloworld"; // works
1
2
3
4
5
6
char char_str_array[] = "helloworld"; // works
char* char_str_point = "helloworld"; // doesn't work
// warning: ISO C++ forbids converting a string constant to 'char*'
const char* const_char_str_point = "helloworld"; // works
const char const_char_str = "helloworld"; // dosen't works
// a value of type "const char *" cannot be used to initialize an entity of type "const char"
1
2
3
4
5
6
string returnString() {return "helloworld";} // works
const string returnConstString() {return "helloworld";} // works
string & returnStringReference() {return "helloworld";} // doesn't work
// a reference of type "std::string &" (not const-qualified) cannot be initialized with a value of type "const char
const string & returnConstStringReference() {return "helloworld";} // doesn't work
// warning: returning reference to temporary

字符串的比较

C风格字符串的比较

使用 strcmp() 需要引入头文件 <cstring>

1
2
3
4
#include <cstring>
...
char str1[] = "zhang", str2[] = "zhang";
cout << strcmp(str1, str2); // 0

需要注意的是直接使用关系运算符比较C风格字符串,实际上比较的是它们的地址而非字符串本身的内容。例如下面代码中的两个字符串str1str2拥有相同的内容,但是 str1==str2 却输出 false。因为比较的是str1str2的首地址,两者的地址显然不相同。

1
2
3
4
char str1[] = "zhang", str2[] = "zhang";
cout << boolalpha; // 格式化bool输出
cout << (str1 == str2); // false
cout << (strcmp(str1, str2) == 0); // true

string类型的比较

string 类型可以直接使用关系运算符 ==!= 进行比较(运算符重载)

1
2
3
string str1 = "zhang", str2 = "zhang";
cout << boolalpha; // 格式化bool输出
cout << (str1 == str2); // true

char数组对中文字符的存储

C语言 使用char字符实现汉字处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <string.h>

int main()
{
char str[] = "你好,世界!Hello,World!";
printf("%s\n", str);

int size = sizeof(str) / sizeof(char);
printf("size = %d\n", size);

printf("%c%c\n", str[0], str[1]);

int i = 0;
while(str[i] != '\0')
{
if(str[i] < 0){
printf("%c%c: %d, %d\n", str[i], str[i+1], str[i], str[i+1]);
i += 2;
}else{
printf("%c: %d\n", str[i], str[i]);
i += 1;
}
}
return 0;
}

struct 结构体

C中的struct

  • C语言中定义结构体的格式

    1
    2
    3
    4
    5
    6
    struct tag
    {
    member-list
    member-list
    ...
    } veriable-list;

    例如

    1
    2
    3
    4
    5
    6
    7
    8
    struct Node  //tag
    {
    int a; //member-list
    char b; //member-list
    } n1,n2; //variable-list
    //声明了一个拥有2个成员的结构体
    //结构体的标签是Node
    //声明了2个结构体变量n1和n2
  • 不注明标签

    1
    2
    3
    4
    5
    6
    7
    8
    struct       //omit the tag
    {
    int a;
    char b;
    } n1,n2;
    //声明了一个拥有2个成员的结构体
    //声明了2个结构体变量n1和n2
    //此结构体没有注明标签
  • 不声明变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Node
    {
    int a;
    char b;
    };
    //声明了一个拥有两个成员的结构体
    //结构体的标签为Node
    //没有声明变量

    struct Node n1,n2[10],*n3; //前面要加struct
    //使用Node标签的结构体,声明变量n1,n2,n3
  • 使用typedef创建新的数据类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct
    {
    int a;
    char b;
    } Node;
    //使用typedef创建新的数据类型Node

    Node n1,n2[10],*n3; //前面不需要加struct
    //使用新数据类型Node,声明变量n1,n2,n3
  • 注意

    • c语言中,函数不能直接作为结构体的成员
    • 当需要函数作为结构体成员时,需要借用函数指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>

    struct Node
    {
    int m;
    void (*p)();
    }

    void fun()
    {
    printf("hello world!\n");
    }

    int main(void)
    {
    struct Node n1= {1, fun}; //初始化结构体
    n1.p(); //p中存放的是函数地址

    return 0;
    }

C++中的struct

  • C++中结构体的定义和C中几乎一样
  • C++中函数可以直接作为结构体成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

struct Student
{
int age;
string name; // string 类型可以作为结构体成员
void greet() {cout << "Hello!";} // 函数可以作为结构体成员
};

int main()
{
// struct Student stu = {18, "zhang wx"};
Student stu = {18, "zhang wx"}; // 结构体实例化+初始化
cout << "name: " << stu.name << endl; // name: zhang wx
cout << "age: " << stu.age << endl; // age: 18
stu.greet(); // Hello!

return 0;
}

在C中使用自定义的结构体类型实例化结构体变量时,需要在结构体名前加上struct关键字;但是在C++中struct可以省略,C++允许直接使用结构体名声明结构体变量,下面的两种实例化结构体的方法在C++中都是可行的

1
2
struct Student stu = {18, "zhang wx"}; // C and C++
Student stu = {18, "zhang wx"}; // C++

内部声明和外部声明

结构声明的位置很重要。对于结构体而言,有两种选择:

  • 内部声明:将声明放在main()函数中;
  • 外部声明:将声明放到main()的前面。

外部声明可以被其后面的任何函数使用,而内部声明只能被该声明所属的函数使用。通常应使用外部声明,这样所有函数都可以使用这种类型的结构。

typedef

使用C++(和C)的关键字typedef来为类型创建别名

1
typedef typeName aliasName;
  • 为char创建别名

    1
    2
    3
    typedef char byte;
    byte b = 'b'; // aliasName
    char c = 'c'; // typeName
  • 为自定义数据类型(结构体)创建别名

    1
    2
    3
    4
    5
    6
    7
    // 可以分开写
    struct Student
    {
    int age;
    string name;
    };
    typedef struct Student Stu;
    1
    2
    3
    4
    5
    6
    // 也可以合在一起
    typedef struct Student
    {
    int age;
    string name;
    }Stu;
    1
    2
    3
    4
    5
    6
    int main()
    {
    Stu stu1 = {18, "zhang wx"}; // aliasName
    struct Student stu2 = {18, "zhang wx"}; // typeNmae
    ...
    }

结构体数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

struct Student
{
int age;
string name;
};

int main(void)
{
// 声明结构体数组
// 使用初始化列表初始化第一个结构体,其余默认为0和\0
struct Student stu[3] = {18, "zhang wx"};

cout << "name: " << stu[0].name << endl; // name: zhang wx
cout << "age: " << stu[0].age << endl; // age: 18

cout << "name: " << stu[1].name << endl; // name:
cout << "age: " << stu[1].age << endl; // age: 0

return 0;
}

结构体成员的访问

结构体实例,使用.运算符访问结构体成员,例如下面的 stu1.namestu1.age
结构体指针,可以先使用*运算符获得结构体实例再使用.运算符访问结构体成员,例如下面的 (*stu2).name(*sut2).age;或者直接用->访问结构体成员,例如下面的 stu2->namesut2->age

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

struct Student
{
string name;
int age;
};

int main(void)
{
struct Student stu1 = {"zhang wx", 18};
cout << "name: " << stu1.name << endl; // name: zhang wx
cout << "age: " << stu1.age << endl; // age: 18

struct Student *stu2 = &stu1;
// *
cout << "name: " << (*stu2).name << endl; // name: zhang wx
cout << "age: " << (*stu2).age << endl; // age: 18
// ->
cout << "name: " << stu2->name << endl; // name: zhang wx
cout << "age: " << stu2->age << endl; // age: 18

return 0;
}

union 共用体

共用体(union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。
union在声明实例时会开辟一片内存,这个内存的大小为其定义里面最大数据类型的大小。当数据类型改变时,内存地址不变,不同的数据类型都会存储在这片内存中。
共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。
例如,假设管理一个小商品目录,其中有一些商品的ID为整数,而另一些的ID为字符串。

  • 在不使用union的情况下,结构体定义中需要声明两个成员,一个int类型(4bytes),一个string类型(32bytes),总共需要占用 32 + 4 = 36 (bytes)。并且在一个结构体实例中,总是会有其中一个成员的空间处于闲置状态;
  • 但如果使用union,那么就可以将两种类型存储在同一片地址中,这样占用的空间大小就是 max(32, 4) = 32 (bytes),如此可以节省空间占用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;

// 定义一个union
union numberUnion
{
int intValue;
double doubleValue;
};

int main(void)
{
// 声明一个union实例
numberUnion n;

// 一个union实例可以存储不同的数据类型,但同一时间只能存储一种类型
// 在这个例子中 n 只有一块内存,既可以存储 int 类型,也可以存储 double 类型,但不能同时存储二者。
n.intValue = 1;
cout << "intValue: " << n.intValue << "\tdoubleValue: " << n.doubleValue << endl;
n.doubleValue = 1.0;
cout << "intValue: " << n.intValue << "\tdoubleValue: " << n.doubleValue << endl;
// intValue: 1 doubleValue: 4.94066e-324
// intValue: 0 doubleValue: 1

// ------------------------------------
// 一个union实例的size是它能存储的最大类型的size,在这个例子中是 8bytes (double)
cout << "sizeof(n): " << sizeof(n) << " bytes" << endl
<< "sizeof(n.intValue): " << sizeof(n.intValue) << " bytes" << endl
<< "sizeof(n.doubleValue): " << sizeof(n.doubleValue) << " bytes" << endl;
// sizeof(n): 8 bytes
// sizeof(n.intValue): 4 bytes
// sizeof(n.doubleValue): 8 bytes

// ------------------------------------
// 声明一个union实例时开辟一个地址,不同类型的数据都存储在这个地址中
cout << "unionAddress: " << &n << endl
<< "intAddress: " << &n.intValue << endl
<< "doubleAddress: " << &n.doubleValue << endl;
// unionAddress: 0x3f10fff988
// intAddress: 0x3f10fff988
// doubleAddress: 0x3f10fff988

// 不同类型数据的内存地址相同
cout << boolalpha; // 格式化输出true(1)和false(0)
cout << "unionAddress == intAddress == doubleAddress: "
<< ((int*)&n == &n.intValue && (double*)&n == &n.doubleValue) << endl;
// unionAddress == intAddress == doubleAddress: true

return 0;
}

enumerate 枚举

1
2
3
4
5
enum spectrum{red, orange, yellow, green, blue, indigo, violet};

cout << red << " " << orange << " " << yellow << " " << green
<< " " << blue << " " << indigo << " " << violet << endl;
// 0 1 2 3 4 5 6
1
enum bits{one=0, two=2, four=4, eight=8};

vector容器

  • 包含头文件

    1
    #include <vector>
  • vector的声明和初始化

    1
    2
    3
    4
    vector<int> v0;           // 空的vector
    vector<int> v1(5); // size为5的vector,未初始化
    vector<int> v2(4, 1); // size为4的vector,初始化为1
    vector<int> v3={1, 2, 3}; // 初始化为 [1, 2, 3]
  • size(): 返回vector对象中元素的个数

  • capacity(): 返回vector对象在内存中最大能够容纳的元素个数

  • assign(): 为vector对象重新赋值

    1
    2
    3
    4
    vector<int> v;
    v.assign(5, 1); // 1 1 1 1 1
    v.assign({3, 2, 1, 0}); // 3 2 1 0
    v = {1, 2, 3, 4};
  • back(): 返回vector对象的末尾元素

  • front(): 返回vector对象的首元素

  • [index]: 使用中括号访问vector对象下标为index的元素

  • clear(): 清空vector对象中的元素

  • empty(): 判断vector对象是否为空,为空返回True,非空返回False

  • push_back(): 向vector对象末尾添加元素

  • pop_back(): 删除vector对象末尾的元素

  • begin(): 返回vector对象首元素的迭代器

  • end(): 返回vector对象末尾元素的迭代器

    1
    2
    3
    4
    5
    vector<int> v = {1, 2, 3, 4, 5};

    vector<int>::iterator i;
    for (i = v.begin(); i != v.end(); i++) cout << *i << endl;
    for (i = v.begin(); i < v.end(); i++) cout << *i << endl;
  • sort(v.begin(), v.end()): 对v中从v.begin()(包括)到v.end()(不包括)的元素进行从小到大的排列

  • reverse(v.begin(), v.end()): 对v中从v.begin()(包括)到v.end()(不包括)的元素进行反转

  • find(v.begin(), v.end(), item): 在v中查找item,找到返回指向第一个item的迭代器,找不到返回v.end()

    1
    2
    3
    4
    5
    6
    vector<int> v = {1, 2, 3, 4, 5};
    reverse(v.begin(), v.end());
    sort(v.begin(), v.end());
    if(find(v.begin(), v.end(), item)!=v.end()) {
    //find the item
    }

    sort(),reverse()和find()方法都需要包含头文件<algorithm>

unordered_map

unordered_map is an associated container that stores elements formed by the combination of a key value and a mapped value. The key value is used to uniquely identify the element and the mapped value is the content associated with the key. Both key and value can be of any type predefined or user-defined. In simple terms, an unordered_map is like a data structure of dictionary type that stores elements in itself. It contains successive pairs (key, value), which allows fast retrieval of an individual element based on its unique key.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 声明一个unordered_map对象
unordered_map<string, int> umap;

// 插入数据
umap["one"] = 1;
umap.insert({"two", 2});
umap.insert(make_pair("three", 3));

// 判断非空
if (umap.empty()){
// Empty
}

// 遍历
for (auto item : umap)
cout << item.first << ": " << item.second << endl;
// 使用迭代器
for (auto iter=umap.begin(); iter != umap.end(); iter++)
cout << iter->first << ": " << iter->second << endl;

// 查找
auto iter = umap.find("one");
if(iter != umap.end()){
cout << iter->first << ": " << iter->second << endl;
}

指针

指针的使用

  • 指向已经声明的变量的空间
    & 为取地址运算符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int var=0; // 声明int型的变量
    int *p; // 声明int型指针
    p = &var; // 将var的地址赋值给p

    // p中存放的是变量var的内存地址,以十六进制表示
    cout << p << endl; // 0x4e0dfffe04
    cout << &var << endl; // 0x4e0dfffe04

    // *p 对指针p所指向的内存进行操作
    *p = 1; // 写入数据
    // var的值也随之改变
    cout << "*p: " << *p << "\tvar: " << var << endl;
    // *p: 1 var: 1
  • 开辟新的空间

    1
    2
    3
    int *p = new int {1}; // 初始化为 1
    cout << *p << endl; // 1
    delete p;
  • 注意下面的声明创建一个int型指针(p1)和一个int变量(p2):

    1
    int *p1, p2;

数组和指针

C和C++内部都是使用指针来处理数组,所以数组和指针基本上是等价的,可以使用操作指针的方式 * 来操作数组,也可以使用操作数组的方式 [] 来操作指针

  • 使用 * 操作数组

    1
    2
    3
    int p[3] = {1, 2, 3};
    cout << p[0] << p[1] << p[2] << endl;
    cout << *p << *(p+1) << *(p+2) << endl;
  • 使用 [] 操作指针

    1
    2
    3
    4
    int *p = new int[3] {0, 1, 2};
    cout << *p << *(p+1) << *(p+2) << endl;
    cout << p[0] << p[1] << p[2] << endl;
    delete [] p

静态数组和动态数组

静态数组就是通常的数组,其内存在编译时分配,存储在栈上

1
int p[3];

动态数组是由new或者malloc声明的数组,其内存在运行时分配,存储在堆上,而不是在编译时分配

1
int *p = new int [3];

动态数组允许你在声明时指定其长度,但是,C++ 没有提供改变此长度的内置方法。你可以通过申请新数组,复制需要的元素,然后删除旧数组的方式绕过这个限制。但是,这种方法已经被证明是容易出错的,尤其是当数组元素是class类型的时候(它们在被创建的时候受特殊的规则管理)。

因此,我们建议你不要这样做。

幸运的是,如果你需要这个功能,C++ 在标准库中提供名为 std::vector 的数据结构,它是一个可调整长度的数组。

指针算术

将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。

  • int 类型的指针 +1,其数值会 +4 —— sizeof(int)

    1
    2
    3
    4
    5
    6
    7
    8
    int p[3] = {1, 2, 3};
    // 连续地址
    cout << "&(*p): " << &(*p) << endl;
    cout << "&(*(p+1)): " << &(*(p+1)) << endl;
    cout << "&(*(p+2)): " << &(*(p+2)) << endl;
    // &(*p): 0x5c4e1ffcf4
    // &(*(p+1)): 0x5c4e1ffcf8
    // &(*(p+2)): 0x5c4e1ffcfc
  • double 类型的指针 +1,其数值会 +8 —— sizeof(double)

    1
    2
    3
    4
    5
    6
    7
    8
    double p[3] = {1, 2, 3};
    // 连续地址
    cout << "&(*p): " << &(*p) << endl;
    cout << "&(*(p+1)): " << &(*(p+1)) << endl;
    cout << "&(*(p+2)): " << &(*(p+2)) << endl;
    // &(*p): 0xfbb6dff6b0
    // &(*(p+1)): 0xfbb6dff6b8
    // &(*(p+2)): 0xfbb6dff6c0

指针的大小

指针大小是由当前CPU运行模式的寻址位数决定,在同一运行环境下不同类型指针的大小相同。

1
2
3
4
5
6
cout << "sizeof(char*): " << sizeof(char*) << " bytes" << endl;
cout << "sizeof(int*): " << sizeof(int*) << " bytes" << endl;
cout << "sizeof(double*): " << sizeof(double*) << " bytes" << endl;
// sizeof(char*): 8 bytes
// sizeof(int*): 8 bytes
// sizeof(double*): 8 bytes

* 星号的三种用法

  • 用于声明指针变量

    1
    int *p = NULL;
  • 地址操作符

    1
    cout << *p << endl;
  • 表示乘法

    1
    2*3

void*

void Pointer in C

A void pointer is a pointer that has no associated data type with it. A void pointer can hold an address of any type and can be typecasted to any type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[])
{
int i = 1; double d = 1.0;
void* ptr;
ptr = &i; // void pointer holds address of int 'i'
printf("i = %d\n", *(int*)ptr);
// printf("i = %d", *ptr);
// warning: dereferencing ‘void *’ pointer
// error: invalid use of void expression

ptr = &d; // void pointer holds address of double 'd'
printf("d = %f\n", *(double*)ptr);

return 0;
}

void*型指针在使用前需要进行类型转换。

const 指针

顶层指针
指针本身是const的(固定的),它指向的一个固定的内存空间,不能改变指针的指向。

1
2
3
4
int a = 1, b = 2;
int * const ptr = &a;
*ptr = 3; // ok
ptr = &b; // error: assignment of read-only variable 'ptr'

底层指针
指针所指向的空间的值是const的(只读的),不能通过指针来改变它所指向内存的值

1
2
3
4
int a = 1, b = 2;
const int * ptr = &a;
ptr = &b; // ok
*ptr = 3; // error: assignment of read-only location '* ptr'

指针使用指向一块固定空间的固定值,既不能改变指针的指向,也不能通过指针改变空间的值。

1
2
3
4
int a = 1, b = 2;
const int * const ptr = &a;
ptr = &b; // error: assignment of read-only variable 'ptr'
*ptr = 3; // error: assignment of read-only location '*(const int*)ptr'

空间申请

malloc和free的用法

1
2
3
4
5
6
7
8
9
// 使用malloc申请空间,返回一个指向该空间的地址,
// 并使用强制类型转换将地址的类型转换成(int*)型
int *p1 = (int*)malloc(sizeof(int));

// 数组: 连续空间的申请
int *p2 = (int*)malloc(5*sizeof(int));

// 使用malloc申请空间,需要使用free释放
free(p1); //单个和连续空间

连续空间的初始化,使用memset函数需要加上头文件,且初始化的值只能是 0 或 -1

1
2
#include <string.h>
memset(p2, 0, 5*sizeof(int));

new和delete的用法

1
2
3
4
5
6
7
8
// 使用new申请空间,返回一段空间的首地址
int *p3 = new int; //注意类型要匹配
// 数组: 连续空间的申请
int *p4 = new int[3]; //new返回这段空间的首地址

// 使用new申请空间,需要使用delete释放
delete p3; //单个空间的释放
delete[] p4; //连续空间的释放

初始化

1
2
3
// 使用new申请空间的时候可以初始化
int *p3 = new int {1}; //初始化为1
int *p4 = new int[3] {1, 2, 3}; //初始化列表

malloc和new的区别

  • 申请单个空间(包括数组和结构体)功能上没有区别
  • 申请对象空间
    • malloc只分配内存,不会进行初始化类成员的工作(调用构造函数),同样free 也不会调用析构函数;
    • new 不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数。

模板类 vector 和 array

引用

引用(Reference)类型是C++相对C新增的一种数据类型。引用就是给已经定义的变量起别名,其主要用途是用作函数的形参,使得函数可以更方便地(相对于指针)对原始变量本身进行操作,而不是只能对其拷贝进行修改。

引用的声明

  • 对普通变量的引用

    1
    2
    3
    int var = 0;
    int &ref1 = var; // 声明变量var的一个引用
    int &ref2 = ref1; // 可以声明引用的引用

    引用声明的时候就要初始化,否则会报错

    1
    2
    3
    int var = 0;
    int &ref;
    // error: 'ref' declared as reference but not initialized

    引用一旦声明就不能再作更改,后续对引用的操作都将等同于直接对变量做更改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int var1 = 0, var2 = 1;
    cout << "&var1: " << &var1 << " " << var1 << endl;
    cout << "&var2: " << &var2 << " " << var2 << endl;
    // &var1: 0xe7193ff864 0
    // &var2: 0xe7193ff860 1
    int &ref = var1;
    cout << "&ref: " << &ref << " " << ref << endl;
    // &ref: 0xe7193ff864 0
    ref = var2; // 这里并非如同指针一样将ref从var1重新指向var2
    // 而是直接将var2的值赋给var1
    cout << "&var1: " << &var1 << " " << var1 << endl;
    cout << "&ref: " << &ref << " " << ref << endl;
    // &var1: 0xe7193ff864 1
    // &ref: 0xe7193ff864 1
  • 引用的地址和被引用变量的地址完全一样,对引用的操作完全等同于对变量的操作

    1
    2
    3
    4
    5
    6
    int var;
    int &ref = var;
    cout << "&var: " << &var << endl;
    cout << "&ref: " << &ref << endl;
    // &var: 0xeceafffbe4
    // &ref: 0xeceafffbe4
  • 常量的引用不能被赋值(因为常量本身不能被修改)

    1
    2
    3
    4
    const int ONE = 0;
    const int &ref1 = ONE;
    ref1 = 1;
    // error: assignment of read-only reference 'ref1'
    1
    2
    3
    const int &ref2 = 2;
    ref2 = 2;
    // error: assignment of read-only reference 'ref2'
  • 数组的引用

    1
    2
    3
    4
    5
    6
    int arr[] = {1, 2, 3};
    int (&ref)[3] = arr;
    // 注意1:(&ref)需要加(),否则根据运算符的优先级,就变成&(ref[3])
    // 注意2:[3]其中的3表示数组的大小,是数组类型的一部分,不能省略
    cout << sizeof(arr)/sizeof(int) << endl; // 3
    cout << sizeof(ref)/sizeof(int) << endl; // 3

    当数组元素省略时int &ref[]可以理解为引用的数组(注意区别于数组的引用),表示定义了一个数组,其中的元素是引用。但是这种用法在C++中是非法的,C++并没有规定引用的数组这一用法。

  • 二维数组的引用

    1
    2
    int arr[2][3];
    int (&ref)[2][3] = arr;
  • 结构体的引用

    1
    2
    3
    4
    5
    6
    7
    8
    struct Student
    {
    int Id;
    string Name;
    };

    struct Student stu; // 声明结构体变量 stu
    struct Student &ref = stu; // 声明结构体变量 stu 的引用 ref
  • 指针的引用

    1
    2
    3
    4
    5
    6
    int* p = new int {0}; // 声明一个指针指向一个new空间,并初始化为0
    int* &ref = p; // 声明指针的一个引用
    cout << "&p: " << &p << ", " << "p: " << p << ", " << "*p: " << *p << endl;
    cout << "&ref: " << &ref << ", " << "ref: " << ref << ", " << "*ref: " << *ref << endl;
    // &p: 0x45a81ff6c0, p: 0x1abbc2d7ea0, *p: 0
    // &ref: 0x45a81ff6c0, ref: 0x1abbc2d7ea0, *ref: 0

引用作参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

void intSquare(int &var)
{
cout << &var << " " << var << endl; // 0x711c3ffa6c 5
var = (var)*(var);
cout << &var << " " << var << endl; // 0x711c3ffa6c 25
}

int main()
{
int x = 5;
cout << &x << " " << x << endl; // 0x711c3ffa6c 5
intSquare(x);
cout << &x << " " << x << endl; // 0x711c3ffa6c 25

return 0;
}

引用作返回值

不要引用局部变量
操作非法内存的结果是未知的。局部变量所占用的内存空间的分配和销毁,取决于编译器的实现,编译器为了优化程序的性能,可能有不同的策略来分配和释放内存。因此出了作用域的局部变量只是不允许访问了,但是该空间可能暂时没有被释放,你还是可以用指针去访问该空间。

  • 引用作返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int& returnArg(int var = 0) { return var; }

    int main()
    {
    int &ref = returnArg();
    // warning: reference to local variable 'var' returned
    cout << ref << endl; // 读取了非法空间,结果不确定,取决于编译器
    return 0;
    }
  • 指针作返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int* returnArg(int var = 0) { return &var; }

    int main()
    {
    int *p = returnArg();
    // warning: address of local variable 'var' returned
    cout << *p << endl; // 读取了非法空间,结果不确定,取决于编译器
    return 0;
    }

引用与指针的区别

  • 引用必须初始化,指针可以不用初始化
  • 引用声明后就不能引用其它空间了,指针可以指向其它空间
  • 引用不占存储空间,指针占存储空间用于存放变量地址
  • 引用效率更高,是对空间的直接访问,指针是间接访问
  • 引用更安全,指针可以偏移*(p+1)
  • 引用只能在C++中使用;指针支持C和C++

交换两个变量的值

  • 使用引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void swapInt(int &a, int &b)
    {
    int temp = a; a = b; b = temp;
    }

    int main()
    {
    int a = 0, b = 1;
    cout << "a=" << a << ", b=" << b << endl; // a=0, b=1
    swapInt(a, b); // 传递引用
    cout << "a=" << a << ", b=" << b << endl; // a=1, b=0
    }
  • 使用指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void swapInt(int* a, int* b)
    {
    int temp = *a; *a = *b; *b = temp;
    }

    int main()
    {
    int a = 0, b = 1;
    cout << "a=" << a << ", b=" << b << endl; // a=0, b=1
    swapInt(&a, &b); // 传递地址
    cout << "a=" << a << ", b=" << b << endl; // a=1, b=0
    }
  • 使用swap函数
    swap()函数是c++内置的函数,包含在std中

    1
    2
    3
    4
    5
    6
    7
    int main()
    {
    int a = 0, b = 1;
    cout << "a=" << a << ", b=" << b << endl; // a=0, b=1
    swap(a, b);
    cout << "a=" << a << ", b=" << b << endl; // a=1, b=0
    }

常量引用 read-only reference

之前我们介绍过常量的引用,常量本身以及常量的引用在初始化之后都不能再被赋值修改,例如

1
2
3
4
const int i = 0;
i = 1; // error: assignment of read-only variable 'i'
const int &r = i;
r = 2; // error: assignment of read-only reference 'r'

但是常量引用不仅可以用于绑定常量,还可以用于绑定变量。这种用法产生的效果是变量本身还是可以被赋值修改,但变量的常量引用(read-only reference)却如同常量一样不能再被赋值修改。

1
2
3
4
int i = 0;
i = 1; // works
const int &r = i;
r = 2; // error: assignment of read-only reference 'r'

递增++递减--运算符

要注意的是前递增(递减)和后递增(递减)

  • 递增运算符

    1
    2
    3
    4
    5
    6
    7
    int a = 20; int b = 20;
    cout << "a = " << a << ": b = " << b << "\n";
    cout << "a++ = " << a++ << ": ++b = " << ++b << "\n";
    cout << "a = " << a << ": b = " << b << "\n";
    // a = 20: b = 20
    // a++ = 20: ++b = 21
    // a = 21: b = 21
  • 递减运算符

    1
    2
    3
    4
    5
    6
    7
    int a = 20; int b = 20;
    cout << "a = " << a << ": b = " << b << "\n";
    cout << "a-- = " << a-- << ": --b = " << --b << "\n";
    cout << "a = " << a << ": b = " << b << "\n";
    // a = 20: b = 20
    // a-- = 20: --b = 19
    // a = 19: b = 19

a++a--)是使用a当前的值参与表达式的计算,然后再将a的值加1(减1)
++b--b)是先将b的值加1(减1),然后再参与表达式的计算

循环

for 循环

  • C++可以把变量的声明写在for()里面

    1
    2
    3
    4
    for(int i = 0; i < 5; i++)
    {
    cout << i << endl;
    }
  • 只有一条语句的时候{}可以省略

    1
    2
    for(int i = 0; i < 5; i += 2)
    cout << i << endl;
  • 使用,逗号分割多个表达式

    1
    2
    for(int i=0, j=0; i+j<10; i+=2, j++)
    cout << i << j << endl;

while 循环

1
2
3
4
5
6
int i = 0;
while(i < 5)
{
cout << i << endl;
i++;
}

while 等待

1
2
3
4
5
6
7
8
9
10
#include <ctime> // describes clock() function, clock_t type
...
float secs=3.0;
clock_t delay = secs * CLOCKS_PER_SEC; // convert to clock ticks
clock_t start = clock(); // record the current time

while (clock() - start < delay) // wait until time elapses
cout << clock() << endl;

cout << "done";

for ⇔ while

在C++中,for和while循环本质上是相同的,只是形式不同,它们可以相互转换。

1
2
3
4
for (init-expression; test-expression; update-expression)
{
statement(s)
}
1
2
3
4
5
6
init-expression;
while (test-expression)
{
statement(s)
update-expression;
}

do while 循环

do while循环不同于for循环和while循环,因为它是出口条件 (exit condition)循环。这意味着这种循环将首先执行循环体,然后再判定测试表达式,决定是否应继续执行循环。如果条件为false,则循环终止;否则,进入新一轮的执行和测试。这样的循环通常至少执行一次,因为其程序流必须经过循环体后才能到达测试条件。下面是其句法:

1
2
3
do
body
while (test-expression);

简单示例

1
2
3
4
5
6
7
8
int n;
cout << "Enter numbers in the range 1-10"
"to find my favotire number:" << endl;
do
{
cin >> n;
} while (n != 7);
cout << "Yes, 7 is my favorite number.";

基于范围的for循环(C++11)

C++11新增了一种循环:基于范围 (range-based) 的for循环。这种用法类似python的for循环,它简化了对数组/容器类的循环操作。

1
2
3
4
5
6
7
8
string fruits[] = {"apple", "banana", "orange"};
for (string fruit : fruits)
{
cout << fruit << endl;
}
// apple
// banana
// orange

break 和 continue

break: 跳过循环的其余部分,结束整个循环
continue: 跳过循环体的其余部分,开始一个新的循环

  • break

    1
    2
    3
    4
    5
    6
    for(int i = 0; i < 5; i++)
    {
    if (i == 2) break;
    cout << i << " ";
    }
    // 0 1
  • continue

    1
    2
    3
    4
    5
    6
    for(int i = 0; i < 5; i++)
    {
    if (i == 2) continue;
    cout << i << " ";
    }
    // 0 1 3 4

分支语句

if/else

1
2
3
4
if (test-condition)
statement1
else
statement2
1
2
3
4
5
6
if (test-condition1)
statement1
else if (test-condition2)
statement2
else
statement3

条件运算符

C++有一个常被用来代替if else语句的运算符,这个运算符被称为条件运算符?:,它是C++中唯一一个需要3个操作数的运算符。该运算符的通用格式如下:

1
expression1 ? expression2 : expression3

如果expressionl为true,则整个条件表达式的值为expression2的值;否则,整个表达式的值为expression3的值。

1
2
5 > 3 ? 10 : 12 // 5 > 3 is true, so expression value is 10
3 == 9? 25 : 18 // 3 == 9 is false, so expression value is 18

switch

  • 范式

    1
    2
    3
    4
    5
    6
    7
    switch (integer-expression) 
    {
    case label1 : statement(s)
    case label2 : statement(s)
    ...
    default : statement(s)
    }
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int idx;
    cout << "Input an integer: ";
    cin >> idx;
    switch (idx%5)
    {
    case 1 : cout << "case 1" << endl; break;
    case 2 : cout << "case 2" << endl; break;
    default: cout << "default" << endl;
    }
  • switch + enum

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    enum color {red, orange, yellow, green, blue};
    int code;
    cout << "Enter color code (0-4): ";
    cin >> code;
    switch (code)
    {
    case red : cout << "red"; break;
    case orange : cout << "orange"; break;
    case yellow : cout << "yellow"; break;
    case green : cout << "green"; break;
    case blue : cout << "blue"; break;
    default: cout << "Out of range";
    }

逻辑运算符

  • &&||!

    1
    2
    3
    statement1 && statement2 // AND
    statement1 || statement2 // OR
    !statement // NOT
  • andornot

    1
    2
    3
    statement1 and statement2 // AND
    statement1 or statement2 // OR
    not statement // NOT
  • 如果 AND 左侧语句为 false,则 C++ 将不会执行判断右侧的表达式

    1
    2
    3
    int a = 0;
    a == 0 && cout << a; // 0
    a != 0 && cout << a; // Nothing
  • 如果 OR 左侧语句为 true,则 C++ 将不会执行判断右侧的表达式

    1
    2
    3
    int a = 0;
    a == 0 || cout << a; // Nothing
    a != 0 || cout << a; // 0

cctype标准库

Standard library header <cctype> | cppreference

文件的读取

函数

  • 没有返回值

    1
    2
    3
    4
    5
    void functionName(parameterList)
    {
    statement(s)
    return; // optional
    }
  • 有返回值

    1
    2
    3
    4
    5
    typeName functionName(parameterList)
    {
    statements
    return value; // value is type cast to type typeName
    }

函数声明、函数原型和函数定义

  • 函数声明(Declaration)
    函数声明是指把函数的名字、函数类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查。

  • 函数原型(Prototyping)
    函数原型是函数声明中的一个特例,若要作为原型,函数声明还必须为函数的参数确定类型和标识符。函数原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。

  • 函数定义(Definition)
    函数定义是指对函数功能的确立,包括指定函数名、函数值类型、形参类型、函数体等,它是一个完整的、独立的函数单位。只有带函数体的声明才叫函数定义。

函数传参

按值传递

C++通常按值传递参数,这意味着将数值参数传递给函数,而函数将其赋给一个新的变量。即便两个变量同名,但它们依然不是同一个变量,它们的内存地址不相同,所以对两者值的修改不会影响到另一方的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

void coutAddress(int arg)
{
cout << "func_arg: " << &arg << " " << arg << endl;
arg = 1;
cout << "func_arg: " << &arg << " " << arg << endl;
}

int main()
{
int arg = 0;
cout << "main_arg: " << &arg << " " << arg << endl;
coutAddress(arg);
cout << "main_arg: " << &arg << " " << arg << endl;

return 0;
}

// main_arg: 0x47725ff98c 0
// func_arg: 0x47725ff960 0
// func_arg: 0x47725ff960 1
// main_arg: 0x47725ff98c 0

注意上面程序的输出结果,mian()中的变量arg和函数coutAddress()中的变量arg,两者同名但内存地址不同。在coutAddress()中对arg进行修改不会影响到main()中的arg。

传递地址

当给函数传递变量地址时,我们可以通过指针操作直接对内存进行修改,这样可以通过函数来修改main()中变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

void intSquare(int* var)
{
*var = (*var)*(*var);
}

int main()
{
int x = 5;
cout << "x: " << x << endl; // 5
intSquare(&x);
cout << "x: " << x << endl; // 25

return 0;
}

传递引用

当给函数传递变量引用时,我们也可以通过引用直接对内存进行修改,从而修改main()中变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

void intSquare(int &var)
{
cout << &var << " " << var << endl; // 0x711c3ffa6c 5
var = (var)*(var);
cout << &var << " " << var << endl; // 0x711c3ffa6c 25
}

int main()
{
int x = 5;
cout << &x << " " << x << endl; // 0x711c3ffa6c 5
intSquare(x);
cout << &x << " " << x << endl; // 0x711c3ffa6c 25

return 0;
}

传递数组

传递常规变量时,函数将使用该变量的值的拷贝;但传递数组时,函数对数组的操作将影响原来的数组。这是因为C++将数组视为指针,所以传递数组等价于传递指针。实际上,这种区别并不违反C++按值传递的方法,函数仍然传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

void squareArr(int arr[], int len)
{
for (int i = 0; i < len; i++)
arr[i] = arr[i] * arr[i];
}

int main()
{
int intArr[] = {1, 2, 3};
int len = sizeof(intArr)/sizeof(int);

for (int i : intArr) cout << i << " "; // 1 2 3
squareArr(intArr, len); cout << endl;
for (int i : intArr) cout << i << " "; // 1 4 9

return 0;
}

参数缺省值(默认参数)

为函数参数指定默认值时,必须从右向左<–连续指定。如果某个参数有默认值,那么它右边的所有参数都必须有默认值,否则会报错error: default argument missing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

void coutArgValues(int i, char c = 'b', float f = 3.14f);

int main()
{
coutArgValues(1); // 1, b, 3.14
return 0;
}

void coutArgValues(int a, char b, float c)
{
cout << a << ", " << b << ", " << c << endl;
}

默认参数可以放在函数声明或者定义中,但不能同时放在声明和定义中,只能放在二者之一,通常都会放在函数声明中。

函数重载(函数多态)

函数重载(Function Overloading)和函数多态(Function Polymorphism)是一回事。C++允许程序员使用不同的参数列表定义多个不同的同名函数,这是C++在C的基础上新增的功能。

函数重载的定义

同一作用域内(公共作用域)函数名相同并且参数列表不同的多个函数互为重载函数

  • Note1: 函数名字相同,但参数必须有所差别,否则会报错重定义。
  • Note2: 调用重载的函数,系统会根据传入参数的差别(类型、数量)自动调用对应的重载函数。
  • Note3: 重载函数的关键是参数列表的区别,在于参数的数目类型顺序,而参数变量名是无关紧要的。
1
2
3
4
5
6
7
8
9
10
// 使用函数重载,实现对输入参数类型的区分
void coutArgType(float f) { cout << "float: " << f << endl; }
void coutArgType(double d) { cout << "double: " << d << endl; }

int main()
{
coutArgType(3.14f); // float: 3.14
coutArgType(3.14); // double: 3.14
return 0;
}

C++中的浮点数
当需要使用float类型时,需要在小数后面加上f,如3.14f2.71828f;对于不加f的小数, C++默认其为double类型,这一点在函数重载时需要注意。

函数重载注意点

  • 同一作用域

    1
    2
    3
    4
    5
    void fun(int a){}   //第1个函数的定义
    void fun(float a){} //第2个函数的定义

    int main() { ... }
    void fun(char a){} //第3个函数的定义

    在主函数内(公共作用域)第1个函数和第2个函数互为重载函数,但不与第3个函数互为重载函数。
    函数声明会扩展函数的作用域,下面的程序中三个函数在主函数内互为重载函数。

    1
    2
    3
    4
    5
    void fun(int a){}   //第1个函数的定义
    void fun(float a){} //第2个函数的定义
    void fun(char a); //第3个函数的声明
    int main() { ... }
    void fun(char a){} //第3个函数的定义
  • 返回值不作为函数重载的条件
    这两种声明方式会报重定义错误

    1
    2
    3
    int fun();
    void fun();
    // cannot overload functions distinguished by return type aloneC/C++(311)
  • 函数重载时谨慎使用默认参数值
    默认参数和函数重载结合使用,可能会造成调用不明确

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void fun(int a, int b, int c = 0);
    void fun(int a, int b);
    int main()
    {
    fun(1, 2);
    return 0;
    }
    // 编译通过, 运行报错
    // error: call of overloaded 'fun(int, int)' is ambiguous

函数的递归调用

C++允许函数自己调用自己,这种功能被称为递归

1
2
3
4
5
6
7
void recurs(argumentlist)
{
statements1
if (test)
recurs(arguments)
statements2
}

斐波那契数列的递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int Fibonacci(int n)
{
if (n==0 or n==1)
return n;
else
return Fibonacci(n-1) + Fibonacci(n-2);
}

int main()
{
for (int i = 0; i < 10; i++)
cout << i << ": " << Fibonacci(i) << endl;
return 0;
}

函数指针(函数作参数)

与数据项相似,函数也有地址。函数的地址是存储其机器语言代码内存的起始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int square(int n) { return n*n; }
int cube(int n) { return n*n*n; }

void coutResult(int n, int (*pf)(int))
{
cout << (*pf)(n) << endl;
// (*pf)等价于函数名
}

int main()
{
for (int i = 0; i < 5; i++)
coutResult(i, square);
for (int i = 0; i < 5; i++)
coutResult(i, cube);
return 0;
}

内联函数

内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。
执行到普通函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着,使用函数时需要一定的时间开销。
C++内联函数提供了另一种选择。内联函数的编译代码将被“内嵌”入其他程序的代码中。也就是说,编译器将使用相应的函数代码替换函数调用。但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。

inline-function-vs-regular-function

上图对比了普通函数和内联函数在调用函数时的区别。左图为普通函数的示例,程序在main()中调用普通函数时会跳到其内存地址,执行函数的代码,在调用结束时再返回main()相应的位置。右图为内联函数的示例。在程序编译完成后,内联函数的代码被嵌入调用的位置,在执行时调用内联函数,程序不会在内存中来回跳动,而是直接线性地执行内联函数的代码。

在使用内联函数时需要在函数声明或者函数定义前加上关键字inline,通常的做法是省略函数原型,将整个定义放在本应提供原型的地方。

1
2
3
4
5
6
7
8
9
inline int square(int n) { return n*n; }

int main()
{
cout << 1 << "^2 = " << square(1) << endl;
cout << 2 << "^2 = " << square(2) << endl;
cout << 3 << "^2 = " << square(3) << endl;
return 0;
}

注意内联函数不能递归定义。

函数模板

如果需要将同一种函数(算法)用于不同类型的参数,除了使用函数重载,C++还新增了一种特性——函数模板。
函数模板是通用的函数描述,它使用泛型(而不是具体类型)来定义函数,其中的泛型可用具体的类型(如int或double)替换。将具体类型的参数传递给模板函数,可使编译器生成该类型的具体函数
比如编写一个交换两个整型int类型变量的swap函数,这个函数就只能传入int型的变量,而无法处理double、char等这些类型,要实现这些类型的交换就要重新编写另一个swap函数,修改它的参数类型。而使用模板就可以让这个函数的实现与类型无关,比如一个swap模板函数,既可以实现对int型变量的交换,又可以实现对double、char等其它型变量的交换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename AnyType>
void swapAnyType(AnyType &a, AnyType &b)
{
AnyType temp = a; a = b; b = temp;
}

int main()
{
int i = 0, j = 1;
swapAnyType(i, j); // 传递int类型变量的引用

double x = 0, y = 1;
swapAnyType(x, y); // 传递double类型变量的引用
}

上面程序的第一行指出,要建立一个模板,并将类型命名为AnyType。关键字 templatetypename 是必需的。另外,必须使用尖括号 <> 。类型名可以任意选择(这里为AnyType),只要遵守C++命名规则即可。余下的代码描述了交换两个 AnyType 类型变量的值。

在标准C++98添加关键字typename之前,C++使用关键字class来创建模板。也就是说,可以这样编写模板定义:

1
2
// template <typename AnyType>
template <class AnyType>

typename关键字使得参数AnyType表示类型这一点更为明显;然而,有大量代码库是使用关键字class开发的。在这种上下文中,这两个关键字是等价的。

注意,函数模板不能缩短可执行程序。对于上面的示例程序,最终结果仍将有两个独立的函数定义,一个int型参数,一个double型参数,就像以手动方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的具体函数。使用模板的好处是,它让生成多个函数定义变得更简单方便、更可靠。

1
2
3
4
5
6
7
8
9
void swapAnyType(int &a, int &b)
{
int temp = a; a = b; b = temp;
}

void swapAnyType(double &a, double &b)
{
double temp = a; a = b; b = temp;
}

模板重载

类似函数重载(多态),模板也可以重载,

单独编译

C++允许甚至鼓励程序员将组件函数放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行的程序。

下面是一个比较两个整数大小的程序,它包含两个函数声明和两个函数定义,写在一个cpp文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//file.cpp
#include <iostream>
using namespace std;

// function prototypes
int intMax(int a, int b);
int intMin(int a, int b);

int main()
{
int i, j;
cout << "Please enter two integers: ";
while(cin >> i >> j)
{
cout << "The Max of them is : " << intMax(i, j) << endl;
cout << "The Min of them is : " << intMin(i, j) << endl;
cout << "Next two numbers (q to quit): ";
}
cout << "Quit!" << endl;
return 0;
}

// function definitions
int intMax(int a, int b){ return a > b ? a : b; }
int intMin(int a, int b){ return a < b ? a : b; }

我们将其拆成三个文件

  • 头文件: 包含函数的声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // head.h
    #ifndef HEAD_H_
    #define HEAD_H_

    // function prototypes
    int intMax(int a, int b);
    int intMin(int a, int b);

    #endif
  • 源代码文件1: 包含main()函数(程序的入口)
    注意,在include头文件时,我们使用双引号"head.h",而不是尖括号<head.h>。如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用双引号而不是尖括号。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // file1.cpp
    #include <iostream>
    #include "head.h" // intMax(), intMin()
    using namespace std;

    int main()
    {
    int i, j;
    cout << "Please enter two integers: ";
    while(cin >> i >> j)
    {
    cout << "The Max of them is : " << intMax(i, j) << endl;
    cout << "The Min of them is : " << intMin(i, j) << endl;
    cout << "Next two numbers (q to quit): ";
    }
    cout << "Quit!" << endl;
    return 0;
    }
  • 源代码文件2:包含函数的定义

    1
    2
    3
    4
    // file2.cpp
    #include "head.h" // intMax(), intMin()
    int intMax(int a, int b){ return a > b ? a : b; }
    int intMin(int a, int b){ return a < b ? a : b; }

在命令行中使用下面的命令对多文件进行编译

1
2
# compile
> g++ -g .\file1.cpp .\file2.cpp -o multiFiles

执行编译后的exe文件

1
2
# execute
> .\multiFiles

头文件

理解 C++ 中的头文件和源文件的作用 <-- 讲解得很详细

下面列出了头文件中常包含的内容。

  • 函数原型
  • 使用 #defineconst 定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数

将结构声明放在头文件中是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性(稍后将介绍),因此可以将其放在头文件中,而不会引起问题。

尖括号和双引号
注意,在include自定义的头文件时,我们使用双引号"head.h",而不是尖括号<head.h>。如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用双引号而不是尖括号。

请不要将函数定义或变量声明放到头文件中:

  • 在头文件中写函数的定义会导致重复定义的错误,如果这个头文件被多个源文件包含。因为每个源文件都会把头文件的内容复制过来,相当于在多个地方定义了同一个函数,这违反了单定义原则。除非函数是内联的,否则这将出错;
  • 在头文件中写函数的定义会增加编译的时间,如果这个头文件被频繁修改。因为每次修改头文件后,所有包含这个头文件的源文件都需要重新编译,这对于大型项目来说非常耗时;
  • 在头文件中写函数的定义会降低代码的可读性和可维护性,如果这个头文件包含了很多函数的定义。因为头文件的主要作用是提供函数的声明和接口,而不是实现细节。把函数的定义放在源文件中,可以让代码结构更清晰,也便于隐藏实现细节和保护数据。

防止头文件重复包含

#include 是一个来自 C 语言的宏命令,它在编译器进行编译之前,即在预编译的时候就会起作用。#include 的作用是把它后面所跟的那个 .h 文件的内容,完完整整地、一字不改地包含到当前的文件中来。值得一提的是,它本身是没有其它任何作用与副功能的,它的作用就是把每一个它出现的地方,替换成它后面所写的那个文件的内容。简单的文本替换,别无其他。

设想 a.h 中声明了 class A , b.h 中声明了 class B, 并且类 B 依赖类 A, 在 b.h 中还包含了a.h #include a.h. 此时我们在源文件中同时 #include a.h#include b.h 会导致 class A 被包含了两次, 这样会报错重定义.

使用预处理器编译指令#ifndef(if not defined),可以避免多次包含同一个头文件。下面的代码片段意味着仅当之前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifindef#endif之间的语句:

1
2
3
4
#ifndef COORDIN_H_
#define COORDIN_H_
...
#endif

#ifndef#endif是标准的C/C++技术,它支持C和C++。

1
2
3
// c++中的方法
#program once
//这种写法非常方便, 但可能部分编译器不支持

全局变量和局部变量

《C++ Primer Plus 第六版》 9.2 存储持续性、作用域和链接性

编译和调试

本节的示例代码使用前面"单独编译"中示例

1
2
3
4
5
6
7
8
9
# folder structure
├── .vscode
| ├── launch.json
| └── tasks.json
├── file.cpp
└── files
├── head.h
├── file1.cpp (main)
└── file2.cpp

基于g++命令

  • 单文件编译

    1
    2
    # compile
    > g++ -g .\file.cpp -o single_file.exe
    1
    2
    # execute
    > .\single_file.exe

    运行.exe文件不要忘记在前面加.\,表示当前目录下
    其中的-g参数表示在编译时生成调试信息,该程序可以被调试器调试

  • 多文件编译

    1
    2
    # compile
    > g++ -g .\file1.cpp .\file2.cpp -o multi_file
    1
    2
    # execute
    > .\multi_file

    .exe 可以省略

基于launch.json和tasks.json

单文件编译和调试

Debug a C++ project in VS Code

Using GCC with MinGW

Run > Add Configuration
> Select environment: C++ (GDB/LLDB)
> Select a configuration: g++.exe - Build and debug active file

You might need to downgrade the C/C++ extension to v1.8.4 since the latest version has some problems that it can not show the drop list for “g++.exe - Build and debug active file”.

There will be a folder named .vscode in your workspace directory, which contains two files as following:

  • .vscode\launch.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    {
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
    "name": "g++.exe - Build and debug active file",
    "type": "cppdbg",
    "request": "launch",
    "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
    "args": [],
    "stopAtEntry": false,
    "cwd": "${fileDirname}",
    "environment": [],
    "externalConsole": false,
    "MIMode": "gdb",
    "miDebuggerPath": "D:\\MinGW\\bin\\gdb.exe",
    "setupCommands": [
    {
    "description": "Enable pretty-printing for gdb",
    "text": "-enable-pretty-printing",
    "ignoreFailures": true
    },
    {
    "description": "Set Disassembly Flavor to Intel",
    "text": "-gdb-set disassembly-flavor intel",
    "ignoreFailures": true
    }
    ],
    "preLaunchTask": "C/C++: g++.exe build active file"
    }
    ]
    }
  • .vscode\tasks.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    {
    "tasks": [
    {
    "type": "cppbuild",
    "label": "C/C++: g++.exe build active file",
    "command": "D:\\MinGW\\bin\\g++.exe",
    "args": [
    "-fdiagnostics-color=always",
    "-g",
    "${file}",
    "-o",
    "${fileDirname}\\${fileBasenameNoExtension}.exe"
    ],
    "options": {
    "cwd": "${fileDirname}"
    },
    "problemMatcher": [
    "$gcc"
    ],
    "group": {
    "kind": "build",
    "isDefault": true
    },
    "detail": "Task generated by Debugger."
    }
    ],
    "version": "2.0.0"
    }

launch.json 是调试的配置文件

  • 参数"program"的值为编译后的.exe文件的路径

    1
    "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",

    其中的${fileDirname}${fileBasenameNoExtension}是vscode的 Predefined variables

    • ${fileDirname} - the current opened file’s folder path
    • ${fileBasenameNoExtension} - the current opened file’s basename with no file extension
    • ${fileWorkspaceFolder} - the current opened file’s workspace folder

    "${fileDirname}\\${fileBasenameNoExtension}.exe"表示一个与当前打开的cpp源代码文件的名称一致的exe文件,比如在当前打开的文件名为myFile.cpp的页面进行调试Run>Start Debugging(F5),那么默认的"program"所指向的就是fileDirname目录下的myFile.exe文件。

    之所以在launch.json中使用"${fileDirname}\\${fileBasenameNoExtension}.exe"作为"program"的值,是因为在默认的task.json中的g++.exe命令的-o参数为${fileDirname}\\${fileBasenameNoExtension}.exe,其使用cpp源代码的文件名命名编译后的exe文件。当然也可以不这么做,而自行命名编译后的文件名称,只是这样自动命名会更方便。

  • 参数"cwd"表示"current working directory",表示调试时的工作目录

    1
    "cwd": "${fileDirname}",
  • 参数 "preLaunchTask" 的值为 task.json 中一个 task 配置的label名。task规定了调试之前的编译工作,调用g++命令生成可执行的exe文件

    1
    2
    3
    // launch.json
    "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
    "preLaunchTask": "C/C++: g++.exe build active file"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // tasks.json
    "label": "C/C++: g++.exe build active file",
    "command": "D:\\MinGW\\bin\\g++.exe",
    "args": [
    "-fdiagnostics-color=always",
    "-g",
    "${file}",
    "-o",
    "${fileDirname}\\${fileBasenameNoExtension}.exe"
    ],

    也可以注释掉launch.json中的"preLaunchTask",自行在终端使用g++ -g ... -o ...命令编译cpp文件,或者先运行一次源代码(程序会自动编译生成可执行exe文件),并指定"program"所指向的编译后的exe文件的路径。

tasks.json 用于在调试之前对cpp文件重新进行编译,生成可以执行和调试的exe文件

  • 参数"command""args"指定了需要调用的命令以及附加的参数

    1
    2
    3
    4
    5
    6
    7
    8
    "command": "D:\\MinGW\\bin\\g++.exe",
    "args": [
    "-fdiagnostics-color=always",
    "-g",
    "${file}",
    "-o",
    "${fileDirname}\\${fileBasenameNoExtension}.exe"
    ],

    ${file}是vscode的 Predefined variables 表示当前打开的文件

    其中g++命令"-o"参数的值和 launch.json 中"program"参数的值一致,都是"${fileDirname}\\${fileBasenameNoExtension}.exe"-o参数指定项目编译后生成的文件名,使用vscode的保留字规定了编译后的exe文件名和源代码cpp文件的名称一致。

  • 参数"label"为一个task配置的标签,launch.json中参数"preLaunchTask"的值就是task的标签,标签唯一标识一个task的配置。

    1
    2
    // launch.json
    "preLaunchTask": "C/C++: g++.exe build active file"
    1
    2
    // tasks.json
    "label": "C/C++: g++.exe build active file"

多文件编译和调试

1
2
3
4
5
6
7
8
9
# folder structure
├── .vscode
| ├── launch.json
| └── tasks.json
├── file.cpp
└── files
├── head.h
├── file1.cpp (main)
└── file2.cpp

针对多文件的调试,我们需要修改task.json中g++-g参数,将原本的${file}(当前打开的cpp文件)修改为多个文件。也可以修改-o参数,设置输出编译后exe文件的路径和名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// .vscode\tasks.json
"tasks": [
{
...
"label": "C/C++: g++.exe build active file",
"command": "D:\\MinGW\\bin\\g++.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
// "${file}",
"${fileWorkspaceFolder}\\files\\file1.cpp",
"${fileWorkspaceFolder}\\files\\file2.cpp",
"-o",
// "${fileDirname}\\${fileBasenameNoExtension}.exe"
"${fileWorkspaceFolder}\\files\\multiFiles.exe"
],
...
}
]

其中多个cpp文件可以用*.cpp来代替,这样更方便

1
2
3
// "${fileWorkspaceFolder}\\files\\file1.cpp",
// "${fileWorkspaceFolder}\\files\\file2.cpp",
"${fileWorkspaceFolder}\\files\\*.cpp",

使用${fileDirname}可以方便定位到文件所在目录

1
"${fileDirname}\\*.cpp",

一个通用的task.json中args可以这样配置

1
2
3
4
5
6
7
"args": [
"-fdiagnostics-color=always",
"-g",
"${fileDirname}\\*.cpp",
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],

-g ${fileDirname}\\*.cpp表示共同编译当前打开的文件所在目录的所有cpp文件;-o ${fileDirname}\\${fileBasenameNoExtension}.exe表示在当前打开的文件所在目录输出与当前打开文件同名的exe文件。

如果在tasks.json中修改了g++-o参数,则需要同时修改launch.json的"program"指向编译后的文件路径

1
2
// .vscode\launch.json
"program": "${fileDirname}\\${fileBasenameNoExtension}.exe",

配置好launch.json和task.json后,在Vscod左侧的Run and Debug栏中选择对应的launch配置的名称,这个例子里面launch配置的名称为"g++.exe - Build and debug active file",然后进行调试Run>Start Debugging(F5)。

Predefined Variables Reference

Predefined variables

C++编译流程

预处理 Preprocess - 编译 Compile - 汇编 Assemble - 链接 linking

  • 预处理: 在这个阶段主要做了三件事: 展开头文件 、宏替换 、去掉注释行
    这个阶段需要GCC调用预处理器来完成, 最终得到的还是源文件, 文本格式

    1
    g++ -E ./helloworld.cpp -o ./helloworld.i # 预处理, 调用 cc1plus 命令
  • 编译: 这个阶段需要GCC调用编译器对文件进行编译, 最终得到一个汇编文件

    1
    g++ -S ./helloworld.i -o ./helloworld.s   # 编译, 调用 cc1plus 命令
  • 汇编: 这个阶段需要GCC调用汇编器对文件进行汇编, 最终得到一个二进制文件

    1
    g++ -c ./helloworld.s -o ./helloworld.o   # 汇编, 调用 as 命令
  • 链接: 这个阶段需要GCC调用链接器对程序需要调用的库进行链接, 最终得到一个可执行的二进制文件

    1
    g++ ./helloworld.o -o ./helloworld.out    # 链接, 调用 ld 命令

最后执行程序

1
2
./helloworld.out # 执行
hello, world!

上面的所有步骤可以使用一次命令完成

1
g++ ./helloworld.cpp -o ./helloworld # (预处理+编译+汇编)链接->可执行文件

上述是单个文件编译生成可执行文件的过程, 对于多个文件也是类似的过程. 先编译生成二进制的动态链接库或者静态链接库, 再将多个 .o, .so 文件进行链接生成可执行文件

graph LR
a.cpp --"preprocess"--> a.i --"compile"--> a.s --"assemble"--> a.o --> ld(("link"))
b.cpp --"preprocess"--> b.i --"compile"--> b.s --"assemble"--> b.o --> ld(("link"))
c.cpp --"preprocess"--> c.i --"compile"--> c.s --"assemble"--> c.o --> ld(("link"))
ld --> abc.out

另外并不是只能使用 g++ 编译 c++ 文件, 只能使用 gcc 编译 c 文件. 事实上 gcc 也可以编译 c++ 文件. 使用 gcc 编译 cpp 文件需要加参数 -l stdc++, 表示在链接时指定使用 C++ 的动态链接库

1
gcc ./helloworld.cpp -o ./helloworld -l stdc++

namespace

命名空间的创建

1
2
3
4
5
6
7
8
9
10
11
12
//创建命名空间stu1,包含函数func和func1
namespace stu1
{
void func(){cout << "stu1::func" << endl;}
void func1(){cout << "stu1::func1" << endl;}
}
//创建命名空间stu2,包含函数func和func2
namespace stu2
{
void func(){cout << "stu2::func" << endl;}
void func2(){cout << "stu2::func2" << endl;}
}

命名空间的使用

1
2
3
4
//使用using指令来打开对应的命名空间,这样就可以直接在main函数中调用命名空间中的函数
//注意这两个语句必须放在两个命名空间stu1和stu2的定义之后,否则会报错
using namespace stu1;
using namespace stu2;
1
2
3
4
5
6
7
8
9
//使用作用域运算符::来调用命名空间中的函数
int mian()
{
//当两个命名空间中包含同名函数时,可以用::来调用指定命名空间的函数
stu1::func();
stu2::func();
//可以不用using指令打开命名空间std,直接使用::调用其中的对象
std::cout << "scope operator::" << endl;
}

类和对象

类和对象的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class CStu
{
private:
// 数据成员
int id_;
string name_;
public:
// 函数成员(方法)
void setId(int id) { id_ = id; }
void setName(string name) { name_ = name; }
int getId() { return id_; }
string getName() { return name_; }
};

int main()
{
CStu stu;
stu.setId(1000);
stu.setName("zhangwx");
cout << "Id: " << stu.getId() << endl;
cout << "Name: " << stu.getName() << endl;
return 0;
}

类成员的访问.&->

类的成员需要通过对象才能访问,不能直接通过类名访问

类对象,使用.运算符访问结构体成员,例如下面的 stu1.setId()stu1.getId()等。
类指针,可以直接用->访问结构体成员,例如下面的 stu2->setId()stu2->getId()
或者先使用*运算符获得结构体实例再使用.运算符访问结构体成员,例如下面的 (*stu2).setName()(*stu2).getName()

1
2
3
4
5
6
7
8
9
10
11
12
// 类对象
CStu stu1;
stu1.setId(1001);
cout << "Id: " << stu1.getId() << endl;
stu1.setName("zhangwx");
cout << "Name: " << stu1.getName() << endl;
// 类指针
CStu *stu2 = new CStu;
stu2->setId(1002);
cout << "Id: " << stu2->getId() << endl;
(*stu2).setName("xiaophai");
cout << "Name: " << (*stu2).getName() << endl;

const函数成员

在类的成员方法声明后面加上const关键字,表示这个类方法是read-only的,它只能读取而不能修改类的数据成员。

1
2
3
4
5
6
7
8
9
class CStu
{
private:
string name_;
public:
void setName(string name) { name_ = name; }
// const方法不能对类的数据成员进行修改,它是read-only的
string getName() const { return name_; }
};

注意事项

  • const只能加在非静态成员函数后面
    如果加到普通函数(非成员函数)后面会报错 a type qualifier is not allowed on a nonmember function,如果放到静态成员函数后面会报错 a type qualifier is not allowed on a static member function

  • 使用 const 限定符的函数成员,其传入的this指针是const的,所以不能通过它修改类的成员(const指针是只读的)

访问修饰符public&private

1
2
3
4
5
6
7
8
9
10
11
12
13
class CStu
{
int name; // 私有数据成员(默认)
public:
void fun1() // 公有函数成员
{ ... }
private:
void fun2() // 私有函数成员
{ ... }
};
//public:使类成员对外可见
//private:成员类内可见,类外不可见
//默认是private:
  • 访问修饰符可以用于类内成员的分类,使得代码更加清晰
  • 类默认的访问修饰符是private:,结构体默认的是public:

类和结构体
类描述看上去很像是包含成员函数以及public和private可见性标签的结构体声明。实际上,C++对结构体进行了扩展,使之具有与类相同的特性(函数可以作为结构体成员)。它们之间唯一的区别是,结构体的默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构体限制为只表示纯粹的数据对象(常被称为普通老式数据(POD,Plain Old Data)结构)。

访问修饰符protected

protected:对类外不可见,对类内和类的子类可见

构造函数constructor

  • 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行;
    构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void;
    构造函数用于对类成员进行初始化,或者进行一些其它操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <iostream>
    using namespace std;

    class CStu
    {
    private:
    int age;
    string name;
    public:
    // 构造函数(与类同名)
    CStu(int age, string name)
    {
    this->age = age;
    this->name = name;
    }
    // 使用接口函数访问私有成员
    int getAge() { return age; }
    string getName() { return name; }
    };

    int main()
    {
    CStu stu = {18, "zhangwx"};
    cout << "Age: " << stu.getAge() << endl;
    cout << "Name: " << stu.getName() << endl;
    return 0;
    }
  • 构造函数在对象定义时被调用

    • 栈区对象(变量对象), 在定义时调用构造函数
    • 堆区对象(指针对象), 在声明指针的时候不会调用, new空间的时候调用
    1
    2
    CStu *stu;       // 声明指针时不调用
    stu = new CStu; // 此处调用构造函数
  • 系统默认构造函数
    一个类除了用户自定义的构造函数,还有一个系统默认的构造函数。系统默认的构造函数是个空函数,什么都不做。只要宏观声明构造函数之后,系统默认的构造函数就被“覆盖”了。

类内声明+内外定义

可以将类的成员函数的定义放在类外,类内只要函数声明。
类外定义成员函数需要加上类名并使用作用域运算符::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class CStu
{
private:
int age;
public:
// 函数声明
CStu(int age);
int getAge();
};

int main()
{
CStu stu = {18};
cout << "Age: " << stu.getAge() << endl;
return 0;
}

// 函数定义
CStu::CStu(int age) {this->age = age;}
int CStu::getAge() { return age; }

当将类的声明放在头文件中时需要这么做,在头文件中类内声明成员函数(包括构造和解析函数),在另外的文件中定义类的成员函数。

  • 类内声明类外定义的意义在于多文件,将函数声明写在头文件,定义写在源文件中。
  • 当只有一个文件的时候没必要将类成员函数的定义写在类外。

初始化列表

与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。

  • 普通变量使用初始化列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <iostream>
    using namespace std;

    class CStu
    {
    private:
    int age_; // 后缀下划线表示私有成员
    string name_; // 并与正常的变量区分造成歧义
    public:
    // 构造函数 // 初始化列表
    CStu(int age, string name) : age_(age), name_(name)
    { }
    int getAge() { return age_; }
    string getName() { return name_; }
    };

    int main()
    {
    CStu stu = {18, "zhangwx"};
    cout << "Age: " << stu.getAge() << endl;
    cout << "Name: " << stu.getName() << endl;
    return 0;
    }
  • 数组成员使用初始化列表
    并非所有的编译器都支持数组的初始化列表,对数组的初始化还是建议使用构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <iostream>
    using namespace std;

    class CStu
    {
    private:
    int scores_[3]; // 使用下划线_与scores进行区分
    public:
    // 构造函数 // 数组的初始化列表
    CStu(int scores[3]) : scores_{scores[0], scores[1], scores[2]}
    { } // 空的函数体,什么都不做
    void coutScores() { for(int i : scores_) cout << i << endl; }
    };

    int main()
    {
    int scores[3] = {80, 90, 100};
    CStu stu = {scores};
    stu.coutScores();
    return 0;
    }
  • 引用类型成员的初始化
    常量和引用类型数据必须使用初始化列表进行初始化,不能使用构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 成员间的引用
    #include <iostream>
    using namespace std;

    class CStu
    {
    private:
    int score_;
    int &mark_;
    public:
    CStu() : mark_(score_) { }
    void setScore(int score) { score_ = score; }
    void setMark(int mark) { mark_ = mark; }
    void coutScore(){ cout << "score: " << score_ << endl; }
    void coutMark(){ cout << "mark: " << mark_ << endl; }
    };

    int main()
    {
    CStu stu;
    stu.setScore(90);
    stu.coutScore(); // score: 90
    stu.coutMark(); // mark: 90

    stu.setMark(80);
    stu.coutScore(); // score: 80
    stu.coutMark(); // mark: 80
    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 引用外部变量
    #include <iostream>
    using namespace std;

    class CStu
    {
    private:
    int &mark_;
    public:
    // mark_ 间接引用外部变量 grade
    // grade <-- score <-- mark_
    CStu(int &score) : mark_(score) { }
    void setMark(int mark) { mark_ = mark; }
    void coutMark(){ cout << mark_ << endl; }
    };

    int main()
    {
    int grade = 80;
    CStu stu = {grade};
    stu.coutMark(); // 80
    cout << grade << endl; // 80

    stu.setMark(90);
    stu.coutMark(); // 90
    cout << grade << endl; // 90
    return 0;
    }
  • 常量成员的初始化
    常量和引用类型数据必须使用初始化列表进行初始化,不能使用构造函数。
    const成员直接传递一个值对其进行初始化即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <iostream>
    using namespace std;

    class CStu
    {
    private:
    const int id_;
    public:
    CStu(int id) : id_(id) // 常量的初始化列表
    { }
    void coutId() { cout << id_ << endl; }
    };

    int main()
    {
    CStu stu = {1000};
    stu.coutId();
    return 0;
    }
  • 结构体的初始化列表

    结构体的初始化列表和普通变量的初始化列表一样,因为结构体变量可以像普通变量一样相互赋值,即C++允许直接把一个结构体变量通过=直接赋值给另一个变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <iostream>
    using namespace std;

    struct Score
    {
    string course;
    int score;
    };

    class CStu
    {
    private:
    Score math_, english_;
    public:
    // 构造函数 // 结构体的初始化列表
    CStu(Score math, Score english) : math_{math}, english_(english) { }
    // 输出变量
    void coutMath() { cout << math_.course << ": " << math_.score << endl; }
    void coutEnglish() { cout << english_.course << ": " << english_.score << endl; }
    };

    int main()
    {
    Score math = {"Math", 80};
    Score english = {"English", 90};
    CStu stu = {math, english};
    stu.coutMath();
    stu.coutEnglish();
    return 0;
    }
  • 初始化列表注意事项

    • 初始化列表的初始化是在构造函数之前执行的
    • 成员变量的初始化顺序只与变量的声明顺序有关,与初始化列表中的书写顺序无关
    • 成员变量之间可以相互初始化,但注意要同类型+初始化顺序
    • 当有多个构造构成重载关系时,初始化列表只执行所绑定的构造函数被执行的初始化列表

析构函数destructor

用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数。与构造函数相对,析构函数完成清理工作,因此实际上很有用。例如,如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。如果构造函数没有new空间,那么析构函数实际上没有需要完成的任务,在这种情况下不需要显示地定义析构函数,编译器会自动生成一个什么都不做的隐式析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
using namespace std;

class CStu
{
private:
// 数据成员
string *name_; // 字符串指针
int *scores_; // 动态数组
int length_; // 动态数组的长度
public:
// 构造函数
CStu(string name, int scores[], int length)
{
length_ = length; // 普通变量初始化
name_ = new string {name}; // new变量空间+初始化
scores_ = new int[length] {}; // new数组空间+初始化为0
// 数组初始化赋值
for(int i=0; i<length; i++) scores_[i] = scores[i];
cout << "Object has been created." << endl;
}

// 析构函数
~CStu()
{
delete name_; // delete普通变量
delete[] scores_; // delete数组变量
cout << "Object has been deleted." << endl;
}

// 函数成员
void coutCStu()
{
cout << "Name: " << *name_ << endl;
for(int i=0; i<length_; i++) cout << scores_[i] << endl;
}
};

int main()
{
int scores[] = {80, 90, 70};
CStu stu = {"zhangwx", scores, 3};
stu.coutCStu();
return 0;
}

析构函数vs构造函数

  • 析构函数比构造函数多一个~符号
  • 析构函数没有参数,构造函数有参数
  • 析构函数只有一个,没有函数重载(因为析构函数没有参数);
    构造函数可以有多个,彼此之间构成重载关系
  • 构造函数在对象定义时执行,析构函数在对象回收时执行
  • 类自带默认构造函数和析构函数,它们什么都不执行

指针对象的创建和释放

当new空间时调用构造函数,delete空间时调用析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

class CStu
{
public:
// Constructor
CStu() { cout << "Object has been created." << endl; }
// Destructor
~CStu() { cout << "Object has been deleted." << endl; }
};

int main()
{
CStu * stu;
stu = new CStu; // Object has been created.
delete stu; // Object has been deleted.
return 0;
}

malloc/free与new/delete的在创建对象上的区别

  • new会触发构造函数,malloc不会
  • delete会触发析构函数,free不会

this 指针

在 C++ 中,this 指针是一个特殊的指针,它指向当前对象的实例。this 是一个隐藏的指针,可以在类的成员函数中使用,它指向当前对象实例,它的值是当前对象的内存地址。
当在类的成员函数中需要用到对象实例的地址时,可以使用 this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class CStu
{
private:
int age_;
public:
CStu(int age) { (*this).age_ = age; }

CStu* getThis() { return this; }
CStu* older(CStu *stu) { return age_ > stu->age_ ? this : stu; }
};

int main()
{
CStu stu1 = {18}, stu2 = {20};
cout << stu1.getThis() << ", " << &stu1 << endl; // 0x393d7ffafc, 0x393d7ffafc
cout << stu2.getThis() << ", " << &stu2 << endl; // 0x393d7ffaf8, 0x393d7ffaf8
cout << stu1.older(&stu2) << endl; // 0x957adff978
cout << stu2.older(&stu1) << endl; // 0x957adff978
return 0;
}

对象数组

类似普通变量类型创建数组,以及结构体创建结构体数组,类也可以创建对象数组,下面是一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

class CStu
{
private:
int age_;
public:
// Constructor
CStu(int age) { this->age_ = age; }
// Member Function
int getAge() { return this->age_; }
};

int main()
{
CStu stus[] = {18, 19, 20}; // creates an array of 3 CStu objects
// Four ways to use the array of objects
for (int i=0; i<3; i++) cout << stus[i].getAge() << endl;
for (int i=0; i<3; i++) cout << (stus+i)->getAge() << endl;
for (int i=0; i<3; i++) cout << (*(stus+i)).getAge() << endl;
for (CStu stu : stus) cout << stu.getAge() << endl;
return 0;
}

运算符重载

类似函数重载,C++不仅允许用户为同一作用域内的同名函数依照参数的差别指定多个定义,还允许为同一运算符根据数据的类型指定不同的操作。

运算符重载示例

下面的程序定义了一个加号+的重载,将同一个类的两个对象进行相加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

class Time
{
private:
int hours; int minutes;
public:
// Constructor
Time(int h=0, int m=0) {hours=h; minutes=m;}
// Operator Overloading
Time operator + (Time &time)
{
Time sum;
sum.minutes = this->minutes + time.minutes;
sum.hours = this->hours + time.hours + sum.minutes/60;
sum.minutes %= 60;
return sum;
}
// Display Time
void display() {cout << hours << " Hours, " << minutes << " Minutes" << endl;}
};

int main()
{
Time t1 = {3, 50}, t2 = {2, 40};
t1.display(); t2.display();

Time t3 = t1 + t2; // operator notation
t3.display();
Time t4 = t1.operator+(t2); // function notation
t4.display();
return 0;
}

注意加号+为一个二目运算符,但是上面重载运算符的定义中只有一个参数。因为对象本身是作为第一操作数(加号左侧),其中传入的对象参数是第二操作数(加号右侧)。

1
Time CStu::operator+(Time &time)

能够重载的运算符

允许重载的运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 双目算术运算符
+(加) -(减) *(乘) /(除) %(取模)
// 关系运算符
==(等于) !=(不等于) <(小于) >(大于) <=(小于等于) >=(大于等于)
// 逻辑运算符
||(逻辑或) &&(逻辑与) !(逻辑非)
// 单目运算符
+(正) -(负) *(指针) &(取地址)
// 自增自减运算符
++(自增) --(自减)
// 位运算符
|(按位或) &(按位与) ~(按位取反) ^(按位异或) <<(左移) >>(右移)
// 赋值运算符
= += -= *= /= %= &= |= ^= <<= >>=
// 空间申请与释放
new delete new[] delete[]
// 其他运算符
()(函数调用) ->(成员访问) ,(逗号) [](下标)

不允许重载的运算符

1
2
3
4
5
6
.       // 成员访问运算符
.* ->* // 成员指针访问运算符
:: // 域运算符
sizeof // 长度运算符
?: // 条件运算符
# // 预处理符号

new和delete运算符

newdeletenew[]delete[]的本质是四个运算符(类似+ - * /

  • newnew[]的函数原型如下

    1
    2
    void * operator new(std::size_t);
    void * operator new[](std::size_t);

    当我们使用newnew[]开辟空间时

    1
    2
    int * pi = new int;
    int * pi = new int[10];

    实际调用的是重载的operator new()operator new[]()

    1
    2
    int * pi = new(sizeof(int));
    int * pi = new(10 * sizeof(int));
  • deletedelete[]的函数原型如下

    1
    2
    void operator delete(void *);
    void operator delete[](void *);

    当我们使用deletedelete[]释放空间时

    1
    2
    delete pi;
    delete pi[];

    实际调用的是重载的operator delete()operator delete[]()

    1
    2
    delete(pi);
    delete[](pi);

new和delete的实现原理-delete是如何知道释放内存的大小的

友元函数与运算符重载

在上面运算符重载的例子中,我们定义了一个加号+的重载作为类Time的一个成员函数,使得类的两个对象可以直接使用加号+进行相加。
现在考虑一种情况,将Time的对象与int型数据进行相加:

  • Time + int:当Time对象在加号+左侧,int数据在加号+右侧时,我们可以直接在类内定义类成员函数的重载运算符,将int类型作为参数。
  • int + Time:当int数据在加号+左侧,Time对象在加号+右侧时,此时直接在类内定义重载运算符已经不能适用了。但是在类外定义非成员函数的重载运算符会导致一个新的问题,即非成员函数不能直接访问类的私有数据,这该怎么解决?

此时可以用到C++的友元函数,通过让类外非成员函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;

class Time
{
private:
int hours; int minutes;
public:
// Constructor with default parameters
Time(int h=0, int m=0) {hours=h; minutes=m;}
// Display Time
void display() {cout << hours << " Hours, " << minutes << " Minutes" << endl;}
// Friend functions
friend Time operator+(Time &time, int &minutes);
friend Time operator+(int &minutes, Time &time);
};

// Operator Overloading
Time operator+(Time &time, int &minutes)
{
Time sum;
sum.minutes = time.minutes + minutes;
sum.hours = time.hours + sum.minutes/60;
sum.minutes %= 60;
return sum;
}
// Function Overloading
Time operator+(int &minutes, Time &time)
{
return time + minutes; // commutative law of addition
}

int main()
{
Time time = {3, 50}; int minutes = 30;
Time sum1 = time + minutes; sum1.display(); // 4 Hours, 20 Minutes
Time sum2 = minutes + time; sum2.display(); // 4 Hours, 20 Minutes
return 0;
}

重载<<运算符

一个很有用的运算符重载是,可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。

在写示例代码之前,我们需要对cout有个更深入的了解,来看看它是如何工作的。例如下面这条语句

1
cout << x << y;

它等同于

1
(cout << x) << y;

cout本身是一个ostream对象,其调用ostream中所定义的opterator<<()方法<< x返回一个ostream对象,然后这个ostream对象再调用opterator<<()进行下一个输出<< y。这就是cout所进行的操作。
我们在了解cout输出变量的原理后,可以对<<进行运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class Time
{
private:
int hours; int minutes;
public:
// Constructor
Time(int h=0, int m=0) {hours=h; minutes=m;}
// Friend function + Operator Overloading
friend ostream& operator<<(ostream &os, Time &time);
};

// Operator Overloading
ostream& operator<<(ostream &os, Time &time)
{
os << time.hours << " Hours, " << time.minutes << " Minutes";
return os;
}

int main()
{
Time time = {3, 50};
cout << time << endl;
return 0;
}

重载>>运算符

在上面重载<<运算符的基础上,我们再对>>运算符进行重载,使得类对象可以通过cin >>进行输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

class Time
{
private:
int hours; int minutes;
public:
// Constructor
Time(int h=0, int m=0) {hours=h; minutes=m;}
// Friend function + Operator Overloading
friend ostream& operator<<(ostream &os, Time &time);
friend istream& operator>>(istream &is, Time &time);
};

// << Operator Overloading
ostream& operator<<(ostream &os, Time &time)
{
os << time.hours << " Hours, " << time.minutes << " Minutes";
return os;
}
// >> Operator Overloading
istream& operator>>(istream &is, Time &time)
{
is >> time.hours >> time.minutes;
return is;
}

int main()
{
Time time = {3, 50};
cout << time << endl; // 3 Hours, 50 Minutes
cin >> time; // 7 40
cout << time << endl; // 7 Hours, 40 Minutes
return 0;
}

类和动态内存

类的静态成员

静态数据成员
类的静态数据成员有一个特点:无论创建了多少个对象,程序都只创建一个静态变量副本。也就是说,类的所有对象共享同一个静态成员。这对于所有类对象都具有相同值的类私有数据是非常方便的。

下面的程序在类中声明了一个静态数据成员Count,并在构造函数中对Count进行++,在析构函数中对Count进行--,用于记录该类的对象数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;

class CStu
{
private:
string Name;
static int Count;
public:
// Constructor
CStu(string name)
{
Name=name;
cout << "Constructor Count: " << Count << "->" << ++Count << endl;
}
// Destructor
~CStu()
{
cout << "Destructor Count: " << Count << "->" << --Count << endl;
}
};

// Initializing static class number
int CStu::Count = 0;

int main()
{
CStu stu1 {"n1"}, stu2 {"n2"}, stu3 {"n3"};
cout << "END" << endl;
return 0;
}
// Constructor Count: 0->1
// Constructor Count: 1->2
// Constructor Count: 2->3
// END
// Destructor Count: 3->2
// Destructor Count: 2->1
// Destructor Count: 1->0

请注意静态成员初始化的的语句

1
int CStu::Count = 0;

这条语句将静态数据成员Count的值初始化为0。请注意,不能在类声明中初始化静态成员变量,这是因为声明只描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。请注意,初始化语句指出了类型int,并使用了作用域运算符::,但没有使用关键字static

初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,这将出现多个初始化语句副本,从而引发错误。

静态函数成员
可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static),这样做有两个重要的后果。
首先,不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在public部分声明的,则可以使用类名和作用域解析运算符来调用它。例如,给上面的CStu类添加一个名为HowMany()的静态成员函数,我们需要在类声明中添加如下原型/定义:

1
static int HowMany() {return Count;}

调用它的方式如下:

1
CStu::HowMany();

其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员,而不能访问对象的非静态成员。例如,静态方法HowMany()可以访问静态成员Count,但不能访问非静态成员Name。

下面是一段完整的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

class CStu
{
static int Count;
public:
string *Name;
// Constructor
CStu(string name) {Count++; Name = new string {name};}
// Destructor
~CStu() {Count--;}
// Static Member Function
static int HowMany() {return Count;}
};

int CStu::Count = 0;

int main()
{
cout << CStu::HowMany() << endl; // 0
{
CStu stu1 {"zhang"}; // calls constructor
cout << CStu::HowMany() << endl; // 1
}
cout << CStu::HowMany() << endl; // 0
return 0;
}

拷贝构造函数

拷贝构造函数(Copy Constructors)用于将一个已有的对象复制到新创建的对象中。它用于新建对象的初始化过程中(包括按值传递参数),而不是常规的赋值过程。

当创建一个新的对象时,如果传入一个已有的对象,那么会调用拷贝构造函数。

拷贝构造函数的原型通常如下:

1
Class_name(const Class_name &);

关于其中的const: 如果在函数中不需要通过引用修改被引用对象(变量)的值,应该声明这个引用为const类型(只读类型),这样可以防止函数内对被引用对象的意外修改。

默认的拷贝构造函数
如果用户没有显示地定义拷贝构造函数,那么系统会调用默认的拷贝构造函数。默认的拷贝构造函数会逐个拷贝非静态成员(浅拷贝),复制的是成员的值。静态成员不受影响,因为它们属于整个类共有,而不是单个对象。下面的程序是个默认拷贝构造函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

class CStu
{
private:
string Name;
public:
CStu(string name) {Name=name;} // Constructor
string getName(){return Name;} // Method
};

int main()
{
CStu stu1 {"zhang"}; // calls constructor
CStu stu2 {stu1}; // calls default copy constructor
cout << stu1.getName() << ", " << stu2.getName() << endl;
return 0;
}

默认拷贝构造函数注意点:对静态成员的操作
如果类中包含静态数据成员,并且其值将在新对象被创建时发生变化,则应该提供一个显式的拷贝构造函数来处理对静态成员的操作。

下面的代码在类中显示地定义了一个拷贝构造函数,用于在对象拷贝初始化时处理静态数据成员。程序中我们声明了一个类的静态数据成员Count,用于对类实例化的对象进行计数,其值在构造函数和拷贝构造函数中自加(初始化时自加),在析构函数中自减(释放空间时自减)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
using namespace std;

class CStu
{
static int Count;
public:
string Name;
// Constructor
CStu(string name)
{
Name=name;
cout << "Constructor Count: "
<< Count << "->" << ++Count << endl;
}
// Copy Constructor
CStu(CStu &stu)
{
Name = stu.Name; // copy
cout << "Copy Constructor Count: "
<< Count << "->" << ++Count << endl;
}
// Destructor
~CStu()
{
cout << "Destructor Count: "
<< Count << "->" << --Count << endl;
}
};

// Initializing static class number
int CStu::Count = 0;

int main()
{
CStu stu1 {"zhang"}; // calls constructor
CStu stu2 {stu1}; // calls copy constructor
cout << stu1.Name << ", " << stu2.Name << endl;
return 0;
}
// Constructor Count: 0->1
// Copy Constructor Count: 1->2
// zhang, zhang
// Destructor Count: 2->1
// Destructor Count: 1->0

如果我们注释掉拷贝构造函数,让在对象拷贝初始化时调用默认的拷贝构造函数,程序的输出会变成下面这样。

1
2
3
4
// Constructor Count: 0->1
// zhang, zhang
// Destructor Count: 1->0
// Destructor Count: 0->-1

导致-1的原因是,在拷贝初始化时程序调用的是默认的拷贝构造函数,而默认的拷贝构造函数并没有像构造函数中那样对静态数据成员Count进行自增,所以虽然我们创建了两个对象,但是Count的值只有1。在程序结束释放空间调用析构函数时,两个对象各自调用一次析构函数,所以Count的值变成-1。

默认拷贝构造函数注意点:按值复制
当类的成员中有指针或者数组时(数组也可以看作指针),调用默认拷贝构造函数会导致问题,默认构造函数对指针的拷贝仅仅拷贝指针本身,即地址,而不是指针所指向的内存空间,这样的拷贝叫做浅拷贝。所以这种情况应当定义一个显示的拷贝构造函数来处理指针和数组成员的拷贝。

下面的程序所声明的类中包含一个指针类型的成员,并且类中并没有显示地定义拷贝构造函数,当调用系统默认的拷贝构造函数进行拷贝初始化时,新创建对象的指针Name仅仅拷贝了旧对象Name的值,两个对象的Name指针指向了同一个地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class CStu
{
public:
string *Name;
// Constructor
CStu(string name) {Name = new string; *Name = name;}
// Destructor
~CStu() {delete Name;}
};

int main()
{
CStu stu1 {"zhang"}; // calls constructor
CStu stu2 {stu1}; // calls copy constructor
cout << *stu1.Name << ", " << *stu2.Name << endl;
// zhang, zhang
cout << stu1.Name << ", " << (stu2.Name) << endl;
// 0x18a27407ea0, 0x18a27407ea0
return 0;
}

上述的默认拷贝构造函数的操作可以如下描述

1
CStu(CStu &stu) {Name = stu.Name;}

它只进行了地址的拷贝,而没有为新建的对象new一个新的string空间,再进行深度拷贝。

我们为这个类显示地定义一个拷贝构造函数来处理指针拷贝的问题,这样问题就得到解决了。

1
2
3
4
5
CStu(CStu &stu)
{
Name = new string;
*Name = *stu.Name;
}

类的赋值运算符

C++允许类对象通过赋值运算符=相互赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:

1
Class_name & Class_name::operator=(const Class_name &);

它接受一个类对象的引用,并返回一个类对象的引用。下面是一个赋值运算符重载的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class CStu
{
public:
string *Name;
// Constructor
CStu(string name) {Name = new string {name};}
// Destructor
~CStu() {delete Name;}
// Assignment Operator Overloading
CStu& operator=(const CStu &stu)
{
cout << "Calling Overloaded Assignment Operator\n";
if (this == &stu) return *this; // object assigned to itself
// this->Name = stu.Name; // shallow copy (DON'T)
*(this->Name) = *(stu.Name); // deep copy
return *this;
}
};

int main()
{
CStu stu1 {"zhang"}; // calls constructor
CStu stu2 {"wang"}; // calls constructor
cout << *stu1.Name << ", " << *stu2.Name << endl;
stu1 = stu2 = stu2; // calls overloaded assignment operator
cout << *stu1.Name << ", " << *stu2.Name << endl;
return 0;
}

在调用构造函数创建类对象的时候允许不加等号,加不加等号是等价的

1
2
3
CStu stu1 {"zhang"};    // calls constructor
CStu stu2 = {"zhang"}; // calls constructor
// CStu stu3 = "zhang"; // error: no suitable constructor exists

在拷贝初始化时加不加等号也是等价的,调用的都是拷贝构造函数,而不是重载的赋值运算符

1
2
3
CStu stu4 {stu1};   // calls copy constructor
CStu stu5 = {stu1}; // calls copy constructor
CStu stu6 = stu1; // calls copy constructor

传入对象类型const&

我们定义一个类,并为这个类定义4个名为copy的方法,这个方法要求传入一个类对象。当对象调用copy方法时,将拷贝传入对象的数据到调用对象。这4个copy方法的区别在于它们的参数类型是否包含常量声明const和引用声明&

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CStu
{
public:
string Name;
// Constructor
CStu(string name) {Name = name;}
// Copy Constructor
CStu(const CStu &stu) {Name = stu.Name;}
// 4 Methods of Copying
void copy1(CStu stu) {Name = stu.Name;}
void copy2(CStu &stu) {Name = stu.Name;}
void copy3(const CStu stu) {Name = stu.Name;}
void copy4(const CStu &stu) {Name = stu.Name;}
};

int main()
{
CStu stu1 {"zhang"}, stu2 {"wang"};
stu1.copy1(stu2);
stu1.copy2(stu2);
stu1.copy3(stu2);
stu1.copy4(stu2);
return 0;
}

这四种声明的区别

  1. 第一种声明会创建一个临时对象stu来接受传入的对象stu2,并调用类的拷贝构造函数对stu进行初始化,最后的操作相当于stu1<-(stu<-stu2)
  2. 第二种声明会创建类CStu的一个引用stu,并使用传入对象stu2对引用stu进行初始化。这种声明没有创建新的对象,也不会调用拷贝构造函数。最后的操作相当于stu1<-stu2
  3. 第三种声明会创建一个const的临时对象stu,并使用传入的对象stu2对const对象stu进行初始化。这种声明会创建新的对象(只不过是const型的),并调用拷贝构造函数。与1的区别在于3的临时对象是const类型的,其值不能被赋值改变。最后的操作相当于stu1<-(stu<-stu2)
  4. 第四种声明会创建类CStu的一个const引用stu(read-only reference),并使用传入的对象stu2对const引用进行初始化。这种声明没有创建新的对象,也不会调用拷贝构造函数。与2的区别在于4的引用是const类型的(read-only),不能通过这个引用对原对象stu2进行赋值操作。注意原对象stu2本身并不是const的,其值可以通过stu2进行修改,只是不能通过它的const型的引用进行修改。最后的操作相当于stu1<-stu2

返回对象类型const&

除了传入对象的类型依据const&可以分成4种,返回对象的类型也同样如此。下面的类依据返回类型的声明是否包含const&定义了4种copy方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CStu
{
public:
string Name;
// Constructor
CStu(string name) {Name = name;}
// Copy Constructor
CStu(const CStu &stu) {Name = stu.Name;}
// Operator Overloading
CStu& operator=(const CStu &stu) {Name = stu.Name; return *this;}
// Method
CStu copy1(const CStu &stu) {Name = stu.Name; return *this;}
CStu& copy2(const CStu &stu) {Name = stu.Name; return *this;}
const CStu copy3(const CStu &stu) {Name = stu.Name; return *this;}
const CStu& copy4(const CStu &stu) {Name = stu.Name; return *this;}
};

int main()
{
CStu stu1 {"zhang"};
CStu stu2 {"wang"};
CStu stu3 {""};
stu3 = stu1.copy1(stu2); // calls copy constructor
stu3 = stu1.copy2(stu2); // doesn't call copy constructor
stu3 = stu1.copy3(stu2); // calls copy constructor
stu3 = stu1.copy4(stu2); // doesn't call copy constructor
return 0;
}

这四种返回类型的区别:

  1. 第一种返回类型声明将返回一个临时对象。它将创建一个临时对象,并使用*this(stu1)对其进行拷贝初始化,这会调用到拷贝构造函数。然后这个临时变量作为返回值又被赋值给stu3,这会调用到赋值运算符重载。

  2. 第二种返回类型声明将返回一个对象的引用。它将创建一个CStu的引用,并使用*this(stu1)对其进行初始化。这个过程没有创建新的对象,并且也不会调用拷贝构造函数。然后返回的stu1的引用又被赋值给stu3,这个过程会调用赋值运算符重载。

  3. 第三种返回类型声明将返回一个const的临时对象。它将创建一个const的临时对象,并使用*this(stu1)对其进行初始化,这会调用到拷贝构造函数。然后这个const的临时变量作为返回值又被赋值给stu3,这会调用赋值运算符重载。与1的区别在于3返回的对象是const类型的,其值不能被赋值修改。

  4. 第四种返回类型声明将返回一个const的对象引用(read-only reference)。它将创建一个CStu的const(read-only)的引用,并使用*this(stu1)对其进行初始化。这个过程没有创建新的对象,并且也不会调用拷贝构造函数。然后返回的stu1的read-only的引用又被赋值给stu3,这个过程会调用赋值运算符重载。与2的区别在于4返回的stu1的const引用是read-only的,不能通过4的返回值对它的引用对象stu1本身进行赋值修改。

在连续调用中,四种返回的区别

1
2
3
4
5
6
7
8
9
10
11
int main()
{
CStu stu1 {"zhang"};
CStu stu2 {"wang"};
CStu stu3 {""};
stu3.copy1(stu2).copy1(stu1); // zhang, wang, wang
stu3.copy2(stu2).copy2(stu1); // zhang, wang, zhang
stu3.copy3(stu2).copy3(stu1); // error: passing 'const CStu' as 'this' argument discards qualifiers
stu3.copy4(stu2).copy4(stu1); // error: passing 'const CStu' as 'this' argument discards qualifiers
return 0;
}

上述的调用相当于

1
(stu3.copy(stu2)).copy(stu1);
  1. 第一种连续调用:stu3.copy1(stu2)返回的是一个临时对象,再使用临时对象的copy1方法拷贝stu1,所以结果上只将stu2赋值给了stu3;
  2. 第二种连续调用:stu3.copy2(stu2)返回的是一个对象引用,并且引用的是stu3,再通过stu3的引用调用copy2方法复制stu1给stu3,所以结果上先将stu2赋值给了stu3,再将stu1赋值给stu3;
  3. 第三种连续调用:stu3.copy3(stu2)返回的是一个const的临时对象,这个const对象调用copy3方法对其值进行修改,这不被允许所以报错。
  4. 第四种连续调用:stu3.copy4(stu2)返回的是一个const的对象引用,并且引用的是stu3,然后程序试图再通过这个const引用对stu3进行修改,但这不被允许所以报错。

何时使用const&

我们定义一个复数类Complex,其中的数据部分放在public下,这是为了方便观察和修改它们。

1
2
3
4
5
6
7
8
class Complex
{
public:
// Real part and Imaginary part
double real; double imag;
// Constructor
Complex(double r=0, double i=0) {real=r; imag=i;}
};

对象vs对象的引用

返回对象的引用
使用引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。如果函数返回作为参数传递给它的对象,可以通过返回它的引用来提高其效率。因为这样不用额外创建一个新的对象。例如,假设要编写函数Max(),它返回两个Complex对象中较大的一个。该函数将以下面的方式被使用:

1
2
3
4
5
6
7
8
9
Complex& Max(Complex &c1, Complex &c2)
{
double modulus1 = (c1.real)*(c1.real) + (c1.imag)*(c1.imag);
double modulus2 = (c2.real)*(c2.real) + (c2.imag)*(c2.imag);
if(modulus1 > modulus2)
return c1;
else
return c2;
}

上面的函数返回Complex&相比于Complex本身效率更高,因为它会返回c1或者c2本身(它们的引用),而不会新创建一个对象。

返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不复存在。在这种情况下应返回对象而不是对象的引用。通常,被重载的算术运算符属于这一类。例如对加号运算符的重载:

1
2
3
4
5
6
7
Complex operator+(Complex &c1, Complex &c2)
{
Complex c;
c.real = c1.real + c2.real;
c.imag = c1.imag + c2.imag;
return c;
}

上面对加号重载的实现,在函数体中定义了一个局部对象c,这个局部对象只在函数被调用时存在,当函数结束调用时会自动调用析构函数释放这个临时对象。所以我们不能返回它的引用只能返回一个新的临时对象。

const对象vs非const对象

返回const对象
上面对加号的重载返回非const的Complex对象,这样返回非const的对象会允许一种特殊的用法,这样的用法显然是不合逻辑的,但编译器会允许这样的操作。

1
c1 + c2 = c3;

它实际上是先计算了c1 + c2并返回了一个非const的临时对象,然后将c3的值赋值给这个非const的临时对象。从结果来看c1、c2、c3的值都没有改变。

或者另一种情况,你错误地将注释中的代码输入成了这样,这种情况却是很常见的错误:

1
2
// if (c1 + c2 == c3)
if (c1 + c2 = c3)

如果我们返回const对象,则可以让编译器对这种情况报错 error: passing 'const Complex' as 'this' argument discards qualifiers

1
const Complex operator+(Complex &c1, Complex &c2);

返回非const对象
两种常见的返回非const对象情形是,重载赋值=运算符以及重载与重载<<运算符。

1
2
3
4
5
6
// Member Function: Operator= Overloading
Complex& operator=(Complex &c)
{
real = c.real; imag = c.imag;
return c;
}

下面三种调用是等价的,c2.operator=(c3)的返回值被赋值给c1

1
2
3
c1 = c2 = c3;
c1 = (c2 = c3);
c1.operator=(c2.operator=(c3));

为此,返回类型为对象Complex或者对象的引用Complex&都是可行的;但是为了提高效率,通过使用引用,可以避免该函数在返回时调用拷贝构造函数重新创建一个对象。

我们这里返回值没有加const,因为我们允许这种情况,c3赋值给c1=c2的返回值

1
(c1 = c2) = c3;

如果声明赋值运算符的重载返回const类型则不能使用这种方式。