进入 C++

main() 函数

1
2
3
4
int main(){
// statements;
return 0;
}
  • 函数头:对函数与程序其他部分的接口进行描述
  • 函数体:一对花括号之间的部分,描述函数的行为
  • 每条完整的指令称为语句,所有的语句都以分号结束
  • 返回语句:结束函数

int main() 描述了 main 函数与操作系统之间的接口

  • main 函数是程序的唯一入口,一个程序有且只有一个 mian 函数
  • main 函数被程序启动代码调用

关于 main 函数的几点说明:

  • int main(void) 无参数时是否加 void 关键字是等价的
  • 避免使用 main()void main() 尽管有时合法
  • main 函数最后的 return 0 可选,但此规定不适用于其他函数

注释

1
2
3
4
5
// 行内注释

/*
多行注释
*/

预处理器

预处理器指令,指示编译器在实际编译之前所需完成的预处理

所有预处理器指令以 # 开头,预处理指令不是 C++ 语句,因此不以分号 “;” 结尾

例如:#include, #define, #typedef

  • #define 预处理指令

    #define PI 3.14159 编译前会将代码中的 PI 替换成 3.14159,但不做类型检查,可使用 const 变量代替

  • 参数宏

    #define MIN(a,b) (a < b ? a : b),但是有潜在问题,可使用内联函数代替

  • #typedef 创建类型别名

    #typedef long long ll

  • #define 创建类型别名存在潜在的问题

    1
    2
    #define FLOAT_POINTER float *
    FLOAT_POINTER pa, pb;

    定义出的 pa 是 float * 型,pb 却是 float

  • typedef 可以避免这个问题,并且功能更强大,参见函数指针

头文件名

头文件类型约定示例说明
C 风格以 .h 结尾math.hC/C++ 都可用
转换后的 C加前缀 c, 无扩展名cmathC++ 可用,还能使用不是 C 的特性,例如 namespace std
C++ 旧式风格以 .h 结尾iostream.hC++ 可用
C++ 新式风格没有扩展名iostreamC++ 可以使用,可以使用 namespace std

名称空间

使用 <iostream> 而非 <iostream.h> 时,应使用名称空间编译指令使 <iostream> 中的定义( cin , endl 等)对程序可用

作用:区分不同库中的同名函数,避免名称冲突

使用方式:

  1. 对象名前加名称空间

    1
    std::cout << "C++ is awesome.";
  2. using namespace std;

    会使 std 名称空间中的所有名称都可用,C++标准库中的函数或对象都是在命名空间 std 中定义的

    可以放在函数体中,只在该函数内起作用

  3. 只使所需的名称可用

    1
    2
    using std::cout;
    using std::endl;

cout: 标准输出流对象

推荐的代码风格

  • 每条语句占一行
  • 每个函数的开始和结束花括号单独占一行,有时开头花括号也可放于函数头行尾
  • 函数中的语句都相对于花括号进行缩进
  • 与函数名称相关的圆括号周围没有空白

变量

变量的命名

命名规则:

  • 只能使用字母,数字和下划线,且不能以数字开头
  • 区分大小写
  • 不能使用关键字
  • 不建议使用下划线开头

命名方案:

  1. 驼峰命名法:第二个单词开始首字母大写
  2. 帕斯卡命名法:所有单词首字母大写
  3. 匈牙利命名法:类型缩写做前缀,如整形n, 字符c, 字符串str/s, 布尔b, 指针p

整型

基本整型:char, short, int, long, long long

char 一般表示字符,又称小整型

short 比 int 宽度小,用于大型整数数组时可节省内存

1 字节 = 8 bit,可以表示 0~255 或 -128~127,char 只占一个字节

无符号整形:类型前面加 unsigned,可以表示更大的值


  • int 通常设为计算机的自然长度,即处理效率最高的长度
  • 常见计算机的int 为4字节 32位,能够表示 -231~231-1,最大值约21亿

sizeof 运算符

运算符 sizeof 可以返回类型或变量的长度,单位为字节

  • sizeof(类型名)
  • sizeof 变量名 或 sizeof(变量名)

sizeof 是运算符,而不是函数

变量初始化

1
2
3
4
5
6
int a = INT_MAX;
int b = 3;
int c = b;
int d(10);
int e = {20};
int f{20};

整型字面值

整型字面值即常量,通常以 10/8/16 进制表示

  • 10/8/16 称为基数
  • 八进制以0开头,如042; 十六进制以0x开头,如0x42;
  • 只是进制的不同,可以互相运算
  • cout 默认输出为10进制,例如 cout << 0x90; 会得到144
  • 使用 cout << oct; //或dec,hex 指定输出时使用的进制
1
2
3
int i = 0x90;
cout << oct << i;
//输出220

指定常量类型

  • 整型常量默认存储为 int 类型

    1
    cout << "Year = " << 1492 << endl;
  • 整数值使用后缀来显式指定类型

    • 以 L/l 后缀表示 long, 如 22022L
    • 以 U/u 后缀表示 unsigned int, 如 22022U
    • 以 UL (不分顺序或大小写)后缀表示 unsigned long
    • ( C++11 ) LL表示 long long, 以及ULL表示unsigned long long

char 类型:字符和小整数

  • char 为 8bit
  • 标准 ASCII码 使用7位二进制数表示 128个字符,剩下一位为0
  • ASCII字符集包含大小写字母,数字,标点符号以及一些控制字符
  • 大写在前,小写在后,'A'-'a' = -32
  • 可以与其他整型运算,以及类型转换,例如int a = 'a';, cout << char('A'+32);

ostream 类有一个成员函数 put()可以用来输出字符,例如 cout.put(‘!’)

char 字面值:

  • 可以将字符用单引号括起来表示字符常量
  • 实际存储的是ASCII码
  • 可以使用转义序列来表示一些键盘无法输入的控制字符
  • 控制字符也可放到单引号中以表示字符常量,如'\n'
  • 可以基于八进制或十六进制使用转义序列

signed char 与 unsigned char:

  • char 默认情况下是否带符号是由C++实现决定的,需要时要显式设置是否有符号
  • unsigned char: 0~255; signed char: -128~127

宽字符类型 wchar_t:

  • wchar_t的长度和符号特征随实现而异,不能满足所有需求
  • C++ 新增了具有特定长度和符号特征的类型 char16_t 和 char32_t
  • char16_t 是无符号的,长16位,用前缀u表示,如 u'C', u"be good"
  • char32_t 是无符号的,长32位,用前缀U表示,如U'R', U"dirty rat"

const 限定符

声明变量时加 const 限定符可以限定变量只读

  • const 限定变量声明时必须初始化

    1
    const int Months = 12;
  • const 比 #define 更优,可以指明类型,可以使用作用域规则将定义限制在特定的函数或文件中,可以用于更复杂的数据类型(如数组和结构)

  • 可以用 const 声明数组长度

浮点数

浮点数(Floating Point)是实数的近似表示

signexponent(8 bits)fraction (23bits)
0011111000100000000000…

表示法:3.45E6, 8.33E-4

浮点类型

三种浮点类型:float, double, long double

  • 通常 float 为32位,double 为64位, long double 为96或128位
  • float 的有效数字为6-7位,double 为15-16位, long double为18-19位

浮点常量

  • 默认为 double 类型
  • 加f或F后缀表示以 float 类型存储,如1.234f, 2.42E20F
  • 加l或L后缀表示以 long double 类型存储,如2.2L

整型和浮点型统称为算术(Arithmetic)类型

算术运算符

  • 整数除法:5/3,会丢弃小数部分
  • 求模运算:5%3,两个操作数必须为整数

类型转换

自动类型转换

自动类型转换发生的时机:

  • 将一种算术类型的值赋给另一种算术类型的变量时

    潜在的问题:

    • 较大的浮点类型转换为较小的浮点类型:精度(有效数位)降低,值可能超出目标类型的取值范围,这种情况下,结果是不确定的
    • 浮点类型转换为整型:小数部分丢失,可能超出范围(C++未定义结果)
    • 较大的整型转换为较小的整型:可能超出范围,通常只复制右边的字节
  • 表达式包含不同的类型时

  • 将参数传递给参数时,实参和形参类型不一致

{}方式初始化时进行的类型转换(C++11):

  • 使用大括号的初始化称为列表初始化(list-initialization)

  • 列表初始化对类型转换的要求更严格,不允许缩窄(narrowing),例如不允许将浮点型转换为整型

  • 使用变量进行初始化的情况:

    1
    2
    3
    4
    int x = 66;
    long y = {x}; //allowed
    char c1 = {66}; //allowed
    char c2 = {x}; //警告或不允许,因为编译器认为x是一个变量,其值可能很大

表达式中的转换:

  • 整型提升

    在计算表达式时,C++将 bool, (signed/unsigned)char 和 short 值转换为 int, 例如

    1
    2
    3
    short chickens = 20;
    short ducks = 35;
    short fow1 = chickens + ducks;

    在计算第三行时,C++先取得 chickens 和 ducks 的值,并把他们转换为 int, 得到的计算结果再转为 int

unsigned short 的整型提升:
如果 short 比 int 段,那么 unsigned short 转为 int;
如果 short 和 int 长度相同,那么 unsigned short 转为 unsigned int

强制类型转换

typename(value)

强制类型转换运算符:

  • static_cast<>

    用于低风险的转换,如整型和浮点型、字符型之间的转换。不能用于不同类型指针、整型与指针之间的转换

  • reinterpret_cast<>

    用于进行不同类型的指针之间、不同类型的引用之间和能容纳指针的数据类型之间的转换

  • const_cast<>

    能够去除 const 属性,例:

    1
    2
    3
    4
    5
    6
    7
    8
    void f(int* num){cout << *num << endl;}

    int main(){
    const int a = 10;
    f(const_cast<int*>(&a));

    return 0;
    }
  • dynamic_cast

    用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性

数组

  • 数组(array) 能够存储多个同类型的值,创建之后大小固定

  • 数组的元素(elements)在内存中依次连续存储

  • 数组的声明:

    typeName arrayName[arraySize];

    • 元素数量arraySize须是整型常量、常量表达式(如 8*sizeof(int) 或const值),在编译时确定数组大小,不能是变量
    • 数组是复合类型,是用其他类型创建的
  • 元素的访问:

    • 使用下标[]访问
    • 下标从0开始,数组末尾下标为n-1
    • 编译器不做下标有效性检查,注意避免越界
  • 初始化

    • 局部变量不初始化,元素的值是不确定的
    1
    int hand(4);
    • 初始化列表:
    1
    int cards[4] = {3, 6, 8, 10};
    • 等号可省略
    • 空大括号可将所有元素置为0
    • 禁止缩窄(narrowing),例如 char c1 {31325}
    • 注意 {1} 只会将第一个元素初始化为1,后面元素还是默认值如 int()

字符串

字符串是在内存中连续存储的一系列字符

C 风格字符串

以空字符 '\0' 结尾,ASCII码为0,是字符串的结束标识

char cat[8] = {'f', 'a', 't', 'e', 's', 's', 'a', '\0'};

  • 字符串常量

    如:"Bubbles"

    • 字符串常量隐含结尾的空字符,用于初始化字符数组时会在末尾自动添加 '\0'

      char fish[] = "Bubbles";

    • 字符数组的大小应该为字符串长度加一

以空白字符分隔的两个字符串常量会拼接成一个

如:cout << "I'd give my right arm to be " "a great violinist.\n"

面向行的输入:

  • cin.getline(array,num)

    用于接收整行字符串并存入array, 读取到 num - 1 个字符,或遇到换行符时停止

    • 丢弃结尾的换行符,在字符串末尾添加空字符'\0'
  • cin.get(array,num)

    类似于cin.getline(),但是不丢弃结尾的换行符

    1
    2
    3
    cin.get(name,ArSize);   //read first line
    cin.get(); //read '\n'
    cin.get(dessert,ArSize) //read second line

cin.get()和cin.getline() 会返回一个cin对象,因此可以连用

常用cin.get(name, Arsize).get()来读取一行

  • 空行和失效位

    • cin.get()读取空行时,会将输入输出流状态设置失效位(failbit)
    • cin.clear()可以重置流状态(goodbit)以恢复输入
    • cin.rdstate()可以查询流状态
    • cin.getline()遇到空行不会设置失效位,但是当读取的字符比指定的多时会设置失效位,导致后续输入被关闭

混合输入数字和字符串:

1
2
3
cin >> year;
cin.getline(address,Size);
//分两行输入year和address,cin读取year后将换行符留在输入队列,导致cin.getline()认为是一个空行

解决方法:

1
2
(cin >> year).get();
cin.getline(address,Size);
  • C 风格字符串常用库函数

    头文件 <cstring>

    • strlen(s1)

      返回字符串长度

    • char* strcpy(char* dest, const char* src)

      复制src到dest,返回一个指向dest的指针

    • char* strcat(char* dest, const char* src)

      将src追加到dest之后,返回一个指向dest的指针,注意长度以及最后的空字符,防止越界

    • int strcmp(const char* str1, const char* str2)

      • 两个字符串相同函数返回零值
      • 第一个字符串排在第二个字符串之前,函数返回负值
      • 第一个字符串排在第二个字符串之后,函数返回正值

      另外还有strncpy(),strncat(),strncmp(),使用第三个参数 n 表示复制/比较前 n 个字符

  • 字符处理函数

    头文件 <cctype>

    函数作用
    isalpha检查是否为字母
    isupper检查是否为大写
    islower检查是否为小写
    isdigit检查是否为数字
    isblank检查是否为空白字符
    tolower将字符转为大写
    toupper将字符转为小写

string 类

头文件: <string>

  • string对象可以使用C风格字符串初始化
  • string对象无需指定大小,可自动调整
  • 可以使用下标索引法访问字符:str[0] = toupper(str[0]);
  • 字符串合并 ( + ),字符串追加 ( += ):str1 += str2

读取一行:

  • getline(cin,str)

常用函数(调用方式 str.fun())

函数描述函数描述
c_str返回字符串的不可修改的C字符数组版本substr返回子串
size返回字符数find查找子串
clear清除内容rfind从后向前查找
insert插入字符compare比较字符
erase移除字符empty检查字符串是否为空
append结尾附加字符replace替换指定部分

原始 (raw) 字符串

所见即所得,保留回车,不转义,允许双引号

1
cout << R"(Jim "King" Tutt uses "\n" instead of endl)" << '\n';
  • 使用前缀R和定界符,前缀R可以和其他前缀结合使用,以标识 wchar_t 等类型的原始字符串,如Ru, UR等
  • 默认定界符为"(和)",如果有冲突,可在引号与括号之间添加任意基本字符作为自定义定界符
1
cout << R"+++("(Who wouldn't?)", she whispered.)+++" << endl;

结构体

结构体是用户自定义的数据类型

定义结构

struct 结构体名 { 结构体成员列表 };

1
2
3
4
5
6
// 使用 struct 关键字定义结构
struct Apple
{
int number;
double weight;
}; // 结尾必须有分号

结构内初始化 ( C++ 11 ):可对所有结构内变量设初值

1
2
3
4
struct Contact {
char name[20] = "";
long long phone = 123;
};

初始化

以上面的 Apple 结构体为例进行初始化

方式 1:顺序初始化

1
Apple A1 = { 1, 0.2 };

方式 2:指定初始化

1
2
3
4
Apple A2 = {
.weight = 0.3;
.number = 2;
}

方式 3:构造函数初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Apple
{
int number;
double weight;

// 使用构造函数给成员变量赋初值,函数名和类名相同,不写返回值,创建对象时自动调用
Apple(int n, double w){
number = n;
weight = w;
}
};

Apple A3(3,0.4);

注意(以下两条对方式 4 也同样适用):

  • 定义了构造函数就不能使用顺序初始化和指定初始化了
  • C++ 11 有构造函数时还可以使用初始化列表。例如 Apple A4 = {4, 0.5};,这会调用构造函数,4 和 0.5 作为构造函数的实参,而非顺序初始化

方式 4:构造函数初始化列表:

  • 可以初始化 const 变量
1
2
3
4
5
6
7
8
9
10
struct Apple
{
const int number;
double weight;

// 构造函数初始化列表,注意不要丢掉冒号,另外,花括号内可以有其他语句
Apple(int n, double w): number(n), weight(w) {}
};

Apple A4(3,0.5);

注意:初始化的顺序由成员变量声明的先后决定,而与构造函数初始化列表中的顺序无关。例如有以下代码:

1
2
3
4
5
6
struct Foo{
int m_x;
int m_y;

Foo(int y): m_y(y), m_x(m_y) {}
};

预期的结果应该是 m_x = m_y = y,但实际上 m_x 先初始化,它将得到未初始化的 m_y 的值

因此,构造函数初始化列表的顺序应当和成员变量的声明顺序保持一致,避免出现此类错误

创建和使用

创建

  1. 在定义结构体时顺便创建结构体变量

    1
    2
    3
    4
    5
    6
    struct Student
    {
    string name;
    int age;
    long long phone;
    } s3; // 此处也可以初始化
  2. 定义之后再创建:Student s1;

  3. 创建时顺便初始化:struct Student s2 = {"李四", 19, 87654321 };

    注:等号可省略( C++ 11 )

使用成员访问运算符 (.) 访问结构变量的成员

1
2
3
4
tom.phone = 12345678901;
strncpy(tom.name, "Tom", sizeof(tom.name));
cout << tom.name;
jerry.name[0] = 'J';

结构体指针

student* p = &s;

  • 利用箭头运算符->可以通过结构体指针访问结构体成员

    p -> name, p -> age

结构嵌套

结构变量可以作为另一个结构类型的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Point {
int x = 0; // C++ 11 结构内初始化
int y = 0;
};

struct Rect {
Point pt1;
Point pt2 = {-1, -1};
};

Rect screen;

cout << screen.pt1.x << screen.pt1.y;
cout << screen.pt2.x << screen.pt2.y;

成员赋值

用户定义的类型与内置类型相似:

  • 结构可作为参数传递给函数
  • 函数可以返回一个结构
  • 可以使用赋值运算符将结构赋值给同类型结构,从而拥有原结构的副本,这称为成员赋值 (memberewise assignment)
1
2
3
4
5
6
7
8
9
10
struct inflatable {
char name[20];
float volume;
double price;
};

inflatable bouquet = {"sunflowers", 0.20, 12.49};
inflatable choice = bouquet;
choice = {"roses", 1.0, 10.5}; // C++11
// 利用结构体可以实现数组的拷贝

结构体数组

创建结构体数组:

1
2
3
4
5
6
struct Student stuArr[3] = 
{
{"张三",18,100},
{"李四",19,80},
{"王五",20,90}
};

给结构体中的元素赋值:

stuArr[2].age = 25;

联合体

定义和使用

1
2
3
4
5
6
7
8
union one4all{
int int_val;
long long_val;
double double_val;
} pail;

pail.int_val = 15;
pail.double_val = 1.38;
  • 联合体可在不同时刻保存不同类型的变量,但是每次只能保存一种
  • 存入新值会覆盖原值
  • 联合体实际是用同一块内存存储不同类型的变量,其大小为最大成员的大小

匿名联合

可在结构体内部定义,像使用结构体的成员变量一样使用它

1
2
3
4
5
6
7
8
9
10
11
12
13
struct widget {
char brand[20];
int type;
union // anonymous union
{
long id_num;
char id_char[20];
};
};

widget prize;
if(prize.type == 1)cin >> prize.id_num;
else cin >> prize.id_char;

枚举

1
enum weekday {SUN, MON, TUE, WED, THU, FRI, SAT};

枚举是一个带名字的整型常量的列表

  • 枚举元素的名称必须互不相同,其值可相同

  • 枚举元素的值是整型常量,其值默认从0开始递增

  • 枚举元素的值可以手动指定,指定一个元素会影响后面元素的默认值

    enum weekday {MON = 1, TUE, WED, THU, FRI, SAT = 0, SUN = 0};
    此时的元素值分别为1,2,3,4,5,0,0

  • 枚举元素是整型,可被提升为 int, 而 int 不会被自动转换为枚举类型

1
2
3
4
5
6
7
8
9
enum weekday {MON = 1, TUE, WED, THU, FRI, SAT = 0, SUN = 0};

weekday workday, weekend;

workday = MON;
weekend = SUN;
weekend = static_cast<weekday>(0);
cout << weekend + 1;
cout << MON + 1;

错误示范

1
2
3
4
weekend = 0;   // int -> weekend
weekend = MON + FRI; // int -> weekend
++weekend; // undefined
++MON; // undefined

指针

创建和使用指针

  1. 计算机存储数据时记录的 3 种基本属性

    • 信息存储的位置、存储的值、存储信息的类型
    • 变量声明指出了值的类型和符号名,程序为值分配内存
    • 取地址运算符 ( & ) 可以获得变量的内存地址(指针✓、const变量✓、常量×、符号常量×)
  2. 用于存储对象地址的数据类型称为指针

  3. 指针的解引用运算符 ( * ) 可以获得该地址存储的值

    1
    2
    3
    4
    int updates = 6;
    int* p_updates = &updates;

    cout << *p_updates << endl;

指针不是整型,不能将整数作为地址值直接赋给指针

const 与指针

左定值,右定向

  • const 在 * 左边:指针指向的变量的值不能修改

    const char* s

  • const 在 * 右边:指针的指向不能修改

    int* const p = &a;

  • const 修饰数组定义:数组的元素不能修改

    const int a[5] = {1, 2, 3, 4, 5};

空指针

定义指针时应将其初始化,否则称为悬浮指针,使用它会引发无法预计的错误

初始化为空指针:

1
2
3
void* p1 = nullptr;  // C++
void* p2 = NULL;
void* p3 = 0;
  • 空指针指向的内存编号是0,而 0~255 之间的为系统占用内存,不允许用户访问,使用多次 delete 操作也不会出问题

  • nullptr 出现的目的是替代 NULL. 在 C++ 中 NULL 被定义为 0,NULL 即是指针,又是数字 0, 当遇到函数重载时可能会出现二义性

    考虑如下函数:

    1
    2
    3
    void foo(char*);
    void foo(int);
    void foo(double);

    那么 foo(NULL); 在调用哪个函数上是有歧义的

使用 new 和 delete 运算符管理动态内存

typeName* pointer_name = new typeName;

new: 在动态内存中为对象分配空间,并返回一个指向该对象的指针

  • 局部变量存储在其作用域(函数)专属的(stack)空间内
    • 栈的空间较小,默认 Win 1M, Linux 8M
  • new 动态分配的内存位于(heap)区
    • 堆的大小取决于系统(虚拟)内存,32 位系统 2~4 GB
  • 计算机可能会没有足够的内存而无法满足 new 的请求
    • 此时 new 会引发一个异常,返回一个空指针

使用 new 动态申请的内存用完后应当使用 delete 进行释放

  • 局部变量在函数调用时创建,函数结束时销毁(回收内存),又称自动变量
  • 动态申请的内存系统不会回收,如果不及时释放会造成内存泄漏 (memory leak)
  • delete 后接指针用与释放指针指向的内存(new 分配的),而非删除指针本身

创建动态数组和动态结构

  1. 动态数组和动态联编

    • 程序运行时通过 new 创建的数组称为动态数组 (dynamic array)
    • 动态数组的长度在运行时确定,确定后大小不可变
    • 通过声明创建的数组,编译时确定数组大小,并为数组分配内存,称为静态联编 (static binding)
  2. 使用 new 创建动态数组

    type_name* ptr = new type_name[num_elements];

    • 分配大小为 sizeof(type_name) * num_elements 的连续内存,并返回首地址,赋给指针 ptr
    • 不能通过 sizeof 运算符获取动态数组的大小
  3. 使用 delete[] 释放内存

    • new 分配的内存务必使用 delete 释放
    • delete 只应释放 new 分配的内存
    • new 分配的内存只能使用 delete 释放一次
    • new[] 为动态数组分配的内存,应使用 delete[] 释放
    • 对空指针使用 delete 是安全的
  4. 指向数组的指针支持与整数的算术运算 (+, -), 以在数组元素间移动

使用 malloc/free 分配/释放内存

头文件:<stdlib.h> /* malloc, free, srand, rand */

例:随机字符串生成

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 <stdio.h>
#include <stdlib.h>
#include <time.h>


int main(){

int n;

printf("How long do you want the string?");
scanf("%d", &n);

char* buffer = (char*)malloc(n+1);
if(!buffer)exit(1);

srand(time(NULL));

for(int i=0;i<n;i++)buffer[i]=rand()%26+'a';

buffer[n] = '\0';

printf("Random String: %s\n", buffer);

free(buffer);

return 0;
}

智能指针简介

C++ 标准库 <memory> 提供 auto_ptr 等智能指针管理动态对象

  • 智能指针行为类似常规指针,但可以通过析构函数自动释放所指向的对象

C++ 11 引入了 shared_ptr, unique_ptr, weak_ptr

  • 其中 shared_ptr 采用引用计数,允许多个智能指针指向同一对象
  • unique_ptr 独占所指向的对象

指针、数组和指针算术运算

  • 数组名代表数组首元素的地址
  • 使用指针比使用数组下标的程序执行速度略快
1
2
3
4
5
6
7
8
9
10
11
12
13
int a[10];

// 数组名代表数组首元素的地址,以下两条语句等价
int* p = a;
int* p2 = &a[0];

int i = 3;

// 访问数组元素的四种等价语法
std::cout << a[i]
<< p[i]
<< *(a+i)
<< *(p+i);
  • 将指针 ip 所指对象值加一

    ++*ip(*ip)++

  • 数组名和指针的区别

    • p = ap++ 合法,a = pa++ 非法
    • sizeof 结果不一样

有效的指针运算:

  1. 同类型指针之间的赋值运算
  2. 指针与整数之间的加减运算:按所指对象的大小偏移
  3. 指向同一数组的两个指针之间的比较和减法运算:
    若 p < q,那么 q - p - 1 就是 p 和 q 之间元素的数目
  4. 用字面量 0 给指针赋值,或指针与 0 之间的比较

指针数组与指向数组的指针

  • 指针数组:int* p1[5];,p1 是一个数组,它的每个元素都是指针

  • 指向数组的指针:int (*p2)[5]; p2 是一个指针,指向一个(具有5个整型元素的)数组。p2 + 1 运算会使 p2 偏移 5 个整型长度

指针与二维数组

对于数组 int a[4],可定义指针 int* p = a,然后使用指针 p 来访问数组元素

对于二维数组 int a[2][3],逻辑上可以看作:

行号\列号012
a[0]a[0][0]a[0][1]a[0][2]
a[1]a[1][0]a[1][1]a[1][2]

可见 a[0], a[1] 都是包含三个元素的数组,a 每偏移 1 个单位,就相当于偏移了 3 个整型元素的长度

因此,数组名 a 对应于一个指向(包含三个元素的)数组的指针 int (*p)[3] = a, 访问数组的元素时 *(*(p + i) + j)a[i][j], p[i][j] 三者是等价的

二维数组一维化:

数组元素在内存中是连续存储的,因此只需要取到首元素的地址,然后就可以按照一维数组的方式使用

a[0][0]a[0][1]a[0][2]a[1][0]a[1][1]a[1][2]

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int a[2][3] = {1,2,3,4,5,6};

// 方式1,取首元素地址,转化为一维数组
int* p = &a[0][0];

for(int i = 0;i < 2*3;i++)
cout << p[i] << endl;

// 方式2,直接当作 a[0][6] 使用,编译器可能有警告
for(int i = 0;i < 2*3;i++)
cout << a[0][i] << endl;

// 方式3,利用地址偏移
for(int i = 0;i < 2*3;i++)
cout << *(*a + i) << endl;

指针和字符串

在 cout 和多数 C++ 表达式中,char 数组名char 指针和引号中的字符串常量都被解释为字符串首字符地址

  • C++ 中的字符串常量是 const char* 类型
  • cout 可以直接输出指针的值,但是字符指针除外,因为会输出它指向的字符串
  • 要输出字符指针的值,需要强制类型转换 cout << (void*)s;
  • 字符串赋给数组也应使用 strcpy()strncpy(),而不是赋值运算符

使用 new 创建动态结构

inflatable *ps = new inflatable;

要使用指针访问成员变量,需要箭头成员运算符 ->

ps -> price 相当于 (*ps).price

自动存储、静态存储和动态存储

三种管理数据内存的方式:自动存储、静态存储和动态存储

  1. 自动存储

    • 在函数内部定义的常规变量使用自动存储空间,称为自动变量
    • 自动变量在所属函数调用时创建,函数结束时自动销毁(回收内存)
    • 自动变量又称局部变量,其作用域为包含它的代码块
    • 自动变量通常存储在(stack)区,栈是后进先出(LIFO)的
  2. 静态存储

    • 整个程序执行期间都存在的存储方式

    • 使变量成为静态存储的两种方式:在函数外部定义变量(全局变量),变量声明时使用 static 关键字(静态变量)

      static double fee;

  3. 动态存储

    • new 分配的内存位于程序的自由存储空间或区(heap)
    • 数据(变量)的生命周期不受函数或程序的生存时间控制
    • 使用 new 和 delete 让程序员控制动态存储的数据
    • new 分配的内存不连续,跟踪新分配内存的位置比较困难

vector 和 array 简介

  • 头文件 <vector><array>

声明:

  • vector<typeName> vectorName(n_elem);

  • array<typeName, n_elem> arrayName;

vector 可以通过 push_back() 增添元素;array 不可以改变长度,且 array 的大小 n_elem 不能是变量