抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

NingLi's Blog

Stay foolish, Stay hungry

C++学习

使用变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
// 姓名,年龄,体重,性别,颜值
string name;
int age;
double weight;
char sex; // X-女 Y-男
bool yz; // 颜值

name = "yifeiliu";
age = 25;
weight = 48.6;
sex = 'X';
yz = true;
}

字符串string类型要使用双引号"",字符类型char要使用单引号''

使用常量

1.宏常量

一般在main函数的上面声明,用大写命名。

语法:#define 常量名 值

2.const修饰的变量

在程序的任何地方都可以声明。

语法:const 数据类型 常量名=值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>         // 包含头文件。
#define MONTHS 12 // 一年中的月份数。
#define PI 3.14159 // 圆周率。

using namespace std; // 指定缺省的命名空间。

// main函数,程序从这里开始执行,每个程序只能有一个main函数。
int main()
{
const int days = 7; // 一个星期中的天数。
cout << "一年有" << MONTHS << "个月。" << endl;
cout << "圆周率的值是:" << PI << endl;
cout << "一个星期有" << days << "天。\n";
}

标识符的命名

1.C++命名规则

C++规定给标识符(变量、常量、函数、结构体、类等)命名时,必须遵守以下规则。

  • 在名称中只能使用字母字符、数字和下划线
  • 名称的第一个字符不能是数字
  • 名称区分大写字符与小写字符
  • 不能将C++关键字用作名称
  • 以下划线和大写字母打头的名称被保留给编译器及其使用的资源使用,如果违反了这一规则,会导致行为的不确定性。
  • C++对名称的长度没有限制,但有些平台可能有长度限制(64字符)。

C++提倡有一定含义的名称(望名知义)

2.C++关键字

关键字也叫保留字,是C++预先保留的标识符。

每个C++关键字都有特殊的含义,用于声明类型、对象、函数、命名空间等,程序中不能声明与关键字同名的标识符。

asm do if return typedef
auto ==double== inline short typeid
==bool== dynamic_cast ==int== signed typename
break else long sizeof union
case enum mutable static unsigned
catch explicit ==namespace== static_cast ==using==
==char== export new struct virtual
class extern operator switch void
==const== ==false== private template volatile
const_cast ==float== protected this wchar_t
continue for public throw while
default friend register ==true==
delete goto reinterpret_cast try

输入数据

程序输入数据的方式有多种。

  • 从控制台的界面中输入(网页、PC桌面程序、APP程序)
  • 从文件中读取
  • 从数据库中读取
  • 从网络中读取
1.用std::cin输入数据

语法:std::cin>>变量名

注意:

  • 布尔型变量的值在计算机内部用1(true)和0(false)存储;程序中可以书写true和false,也可以书写1和0,其它值将强制转换成1;用cin输入时可以填1和0,其它值也强制转换成1;用cout输出时只显示1和0,不显示true和false
  • 如果输入的数据与变量的数据类型不匹配,会导致行为的不确定性
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; // 指定缺省的命名空间。
// main函数,程序从这里开始执行,每个程序只能有一个main函数。
int main(){
string name; // 姓名。
cout << "请输入超女的姓名:";
cin >> name;
cout << "输入的超女姓名是:" << name << endl;

int age; // 年龄。
cout << "请输入超女的年龄:";
cin >> age;
cout << "输入的超女年龄是:" << age << endl;

double weight; // 体重(kg)。
cout << "请输入超女的体重(kg):";
cin >> weight;
cout << "输入的超女体重是:" << weight << endl;

char sex; // 性别:X-女;Y-男。
cout << "请输入超女的性别(X-女;Y-男):";
cin >> sex;
cout << "输入的超女性别是:" << sex << endl;

bool yz; // 颜值:true-漂亮;false-不漂亮。
cout << "请问输入超女的颜值(1-漂亮;0-不漂亮):";
cin >> yz;
cout << "输入的超女颜值是:" << yz << endl;
}

逗号运算

1.逗号运算

把一行语句中的多个表达式连接起来,程序将从左到右执行表达式。

语法:表达式一, 表达式二, ……, 表达式n;

逗号运算常用于声明多个变量

1
2
int a,b;      // 声明变量a和b。
int a=10,b=20; // 声明变量a和b并初始化。

也可以用于其它语句中,但是,逗号运算符是所有运算符中级别最低的,以下两个表达式的效果是不同的。

1
2
3
int a,b; 
b=a=2,a*2;
b=(a=2,a*2);
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// main函数,程序从这里开始执行,每个程序只能有一个main函数。
int main()
{
int a, b;
b = a = 2, a * 2;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
b = (a = 2, a * 2);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
}
1
2
3
4
a=2
b=2
a=2
b=4

运算的优先级

1.运算的优先级

一个表达式可以包含多个运算符,运算符的优先级决定了表达式各部分的执行顺序。

例如,按照运算规则, 的优先级比+高,所以的b c将先执行:

1
a + b * c;

如果想让a + b先执行,则必须使用括号:

1
(a + b) * c;

如果一个表达式中操作符的优先级相同,那么它们的结合律(associativity)决定了它们的执行顺序(从左到右或从右到左)。

例如,算术运算的组合方式是从左到右,赋值运算则是从右到左。如下:

表达式 结合律 组合方式
a/b%c 从左到右 (a/b)%c
a=b=c 从右到左 a=(b=c)

下表是全部运算符的优先级结合律

优先级 运算符 名称或含义 使用形式 结合方向
1 [] 下标 地址[表达式] 左到右
1 () 圆括号 (表达式)/函数名(形参表) 左到右
1 . 成员选择(对象) 对象.成员名 左到右
1 -> 成员选择(指针) 对象指针->成员名 左到右
2 - 负号运算符 -表达式 右到左
2 (类型) 强制类型转换 (数据类型)表达式 右到左
2 ++ 前置自增运算符 ++变量名 右到左
2 ++ 后置自增运算符 变量名++ 右到左
2 -- 前置自减运算符 --变量名 右到左
2 -- 后置自减运算符 变量名-- 右到左
2 * 取值运算符 *指针变量 右到左
2 & 取地址运算符 &变量名 右到左
2 ! 逻辑非运算符 !表达式 右到左
2 ~ 按位取反运算符 ~表达式 右到左
2 sizeof 长度运算符 sizeof(表达式) 右到左
3 / 表达式/表达式 左到右
3 * 表达式*表达式 左到右
3 % 余数(取模) 整型表达式/整型表达式 左到右
4 + 表达式+表达式 左到右
4 - 表达式-表达式 左到右
5 << 左移 变量 左到右
5 >> 右移 变量>>表达式 左到右
6 > 大于 表达式>表达式 左到右
6 >= 大于等于 表达式>=表达式 左到右
6 < 小于 表达式 左到右
6 <= 小于等于 表达式 左到右
7 == 等于 表达式==表达式 左到右
7 != 不等于 表达式!= 表达式 左到右
8 & 按位与 表达式&表达式 左到右
9 ^ 按位异或 表达式^表达式 左到右
10 | 按位或 表达式|表达式 左到右
11 && 逻辑与 表达式&&表达式 左到右
12 || 逻辑或 表达式||表达式 左到右
13 ?: 条件运算符 表达式1? 表达式2: 表达式3 右到左
14 = 赋值运算符 变量=表达式 右到左
14 /= 除后赋值 变量/=表达式 右到左
14 *= 乘后赋值 变量*=表达式 右到左
14 %= 取模后赋值 变量%=表达式 右到左
14 += 加后赋值 变量+=表达式 右到左
14 -= 减后赋值 变量-=表达式 右到左
14 <<= 左移后赋值 变量 右到左
14 >>= 右移后赋值 变量>>=表达式 右到左
14 &= 按位与后赋值 变量&=表达式 右到左
14 ^= 按位异或后赋值 变量^=表达式 右到左
14 |= 按位或后赋值 变量|=表达式 右到左
15 , 逗号运算符 表达式,表达式,… 左到右

注意:

  • 如果不确定运算符的优先级,可以加括号
  • 多用括号,让代码的可读性更好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>         // 包含头文件
using namespace std; // 指定缺省的命名空间
// main函数,程序从这里开始执行,每个程序只能有一个main函数
int main()
{
int a, b, c;
c = 10; // 赋值表达式的值为赋值符号右边的值。
cout << "(c = 10)=" << (c = 10) << endl;
a = b = c = 10; // 赋值运算的结合律是从右到左。

int a, b, c, d;
a = 4, b = 2, c = 5;
d = a * (b / c); // 算术运算的结合律是从左到右。
cout << "d=" << d << endl;
}

多条件的if语句

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (表达式一){ 
// 表达式一为真时执行的语句。
}
else if (表达式二){
// 表达式二为真时执行的语句。
}
else if (表达式三){
// 表达式三为真时执行的语句。
}
……
else if (表达式n){
// 表达式n为真时执行的语句。
}
else
{
// 全部表达式都不为真时执行的语句。
}

注意:

  • 多条件的if语句本质上是嵌套的if语句
  • 最多只能有127个条件分支
  • 最后一个else可以没有

switch语句

switch也是一种选择结构的语句,可以代替简单的多条件的if语句。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (表达式)
{
case 值一:
语句一;
break;
case 值二:
语句二;
break;
......
case 值n:
语句n;
break;
default:
上述条件都不满足时执行的语句;
}

注意:

  • case后面必须是整数和字符,或者是结果为整数和字符的表达式,但不能使用变量
  • default不是必须的,当没有default时,如果全部的case匹配失败,那么就什么都不执行
  • 每个分支不要漏写break;语句

循环的跳转

breakcontinue两个关键字用于控制循环体中代码的执行流程。

break跳出(中止)当前循环语句。

continue回到当前循环语句的首部。

示例:

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; // 指定缺省的命名空间
int main(){
// break跳出(中止)当前循环语句,continue回到当前循环语句的首部。
// 程序运行后一直工作,逐个输入超女的数据,判断是否晋级,如果到了休息时间,就把程序停下来。
// 超女选秀的流程:1)如果漂亮,直接晋级;2)不漂亮也行,身材火辣的也可以晋级。
bool once = true; // 是否为第一次执行循环。
while (true){
if (once == false){
// a)显示“是否继续下一名超女选秀(1-继续,0-结束):”的提示文字。
cout << "是否继续下一名超女选秀(1-继续,0-结束):";
// b)输入是否继续的决定,存放在变量中。
bool exist; cin >> exist;
// c)判断输入的决定,如果是结束,流程跳出循环。
if (exist == false) break;
}
once = false; // 表示循环已经被执行过。
// 1)显示“请输入超女的颜值(1-漂亮,0-不漂亮):”的提示文字。
cout << "请输入超女的颜值(1-漂亮,0-不漂亮):";
// 2)输入超女的颜值,存放在变量中。
bool yz; cin >> yz;
// 3)判断超女的颜值,如果漂亮,显示“晋级成功”,流程跳转到循环的首部。
if (yz == true){
cout << "晋级成功\n"; continue;
}
// 4)显示“请输入超女的身材(1-火辣,0-不辣):”的提示文字。
cout << "请输入超女的身材(1-火辣,0-不辣):";
// 5)输入超女的身材,存放在变量中。
bool sc; cin >> sc;
// 6)判断超女的身材,如果火辣,显示“晋级成功”。
if (sc == true) cout << "晋级成功\n";
}
}

for循环语句

语法:

1
2
3
for (语句一 ; 表达式 ; 语句二){
语句块
}

1)循环开始的时候,先执行语句一,在整个循环过程中语句一只会被执行一次

2)计算表达式的值,如果为真,就执行一次循环体中的语句块

3)执行完语句块后,执行一次语句二

4)重复第2)步和第3),直到表达式的值不为真才结束for循环

注意:

  • 不要纠结for循环与while循环的区别,它们本质上没有区别
  • for循环一般需要一个相当于计数器的变量,在语句一中对它进行初始化,在语句二中进行计数操作
  • 在for循环的语句一中,可以声明计数器变量
  • 在for循环中,语句一、表达式和语句二都可以为空,for (; ; )等同于while (true)
  • continue和break两个关键字也可以用在for循环体中。

示例:

1
2
3
4
5
6
7
8
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
// 有十个超女,编号是1-10,在控制台输出这十个超女的编号。
for (int no = 1; no <= 10; no++){
cout << "这是第" << no << "名超女的编号。\n";
}
}

函数基础

在复杂的程序中,如果全部的代码都写在main函数中,main函数体将非常庞大臃肿。

把任务分工到其它的函数中,main函数只负责程序的核心流程,具体的任务由其它函数完成。

这种思想就是模块化编程。

声明和定义函数的语法:

1
2
3
4
5
6
返回值的数据类型 函数名(参数一的数据类型 参数一, 参数二的数据类型 参数二,……)
{
实现函数功能的代码
…………
return 返回值;
}

函数的声明:让编译器知道函数的存在,包括返回值的数据类型、函数名和参数列表。

函数的定义:函数的实现过程。

注意:

  • 函数的声明和定义可以书写在一起,也可以分开,如果书写在一起,一般放在main函数的上面,如果分开,一般在main函数的上面声明,在main函数的下面定义
  • 如果函数的声明和定义分开书写,函数的声明后面一定要有分号,函数的定义后面一定不能写分号
  • 在同一个程序中,函数只需要声明和定义一次,也可以多次声明,但只能定义一次
  • 函数的声明必须和函数的定义一致(返回值的数据类型、函数名和参数列表),如果函数名和参数列表不同,表示它们不是同一个函数。l return语句返回值的数据类型必须与函数的声明一致
  • 在函数体中,return语句可以多次使用
  • 如果函数的重点是实现功能,不关心返回值,返回值的数据类型填void,return语句后面就空着
  • 函数可以没有任何参数
  • 函数名是标识符,必须满足标识符的命名规则
  • 在函数的声明和函数的定义中,参数命名可以不同,但是没必要这么书写。
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; // 指定缺省的命名空间。
// 写一个函数,给它两个整数,让它比较两个整数的大小,返回较大的那个整数。
int max(int a, int b); // 函数声明后面的分号不能少。
// 写一个函数,给它一个字符串,让它在控制台显示出来。
void print(string str);
// 写一个函数,在控制台输出九九乘法表。
void printmt();
int main(){
}
int max(int a, int b){ // 函数定义后面不能加分号。
if (a > b) return a;
return b;
}
void print(string str){
cout << str << endl;
return;
}
void printmt(){
// 在控制台输出九九乘法表。
for (int ii = 1; ii <= 9; ii++){
for (int jj = 1; jj <= ii; jj++){
cout << ii << "*" << jj << "=" << ii * jj << " ";
}
cout << endl;
}
return;
}

函数的调用

1
语法:函数名(参数一,参数二,……)

注意:

  • 声明函数的代码必须放在调用之前,定义函数的代码可以放在调用之后
  • 调用函数的时候,参数列表必须与函数的声明一致(参数的个数、书写的顺序和数据类型)
  • 不管在什么地方,都不能调用main函数,但是,在普通函数中,可以调用其它的普通函数
  • 调用函数的代码可以独占一条语句,也可以用于表达式(赋值运算、算术运算、关系运算、函数的参数)
  • 如果函数用于表达式中,返回值的数据类型要匹配(否则可能会被隐式转换或编译错误)
  • 如果函数有返回值,可以不关心它,忽略它
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; // 指定缺省的命名空间。
// 写一个函数,给它两个整数,让它比较两个整数的大小,返回较大的那个整数。
int max(int a, int b); // 函数声明后面的分号不能少。
// 写一个函数,给它两个参数:no-超女编号,str-表白的内容。
void print(int no,string str);
// 写一个函数,在控制台输出九九乘法表。
void printmt();
int main(){
cout << "max(5,8)=" << max(5,8) << endl;
print(8, "请借给我五分钱吧。");
printmt();
}
int max(int a, int b){ // 函数定义后面不能加分号。
if (a > b) return a;
return b;
}
void print(int no, string str){
cout << "亲爱的"<< no<<"号:"<< str << endl;
return;
}
void printmt(){
// 在控制台输出九九乘法表。
for (int ii = 1; ii <= 9; ii++){
for (int jj = 1; jj <= ii; jj++){
cout << ii << "*" << jj << "=" << ii * jj << " ";
}
cout << endl;
}
return;
}

变量的作用域

作用域是指程序中变量存在(或生效)的区域,超过该区域变量就不能被访问。

变量分全局变量局部变量两种,==全局变量在整个程序中都可以访问,局部变量只能在函数或语句块的内部才能访问==。

C++中定义变量的场景主要有五种:

  • 在全部函数外面定义的是全局变量
  • ==在头文件中定义的是全局变量==
  • 在函数和==语句块==内部定义的是局部变量
  • 函数的参数是该函数的局部变量
  • 函数内部用static修饰的是静态局部变量
1.全局变量

在整个程序生命周期内都是有效的,在定义位置之后的任意函数中都能访问。

全局变量在主程序退出时由系统收回内存空间。

2.局部变量

在函数或语句块内部的语句使用,在函数或语句块外部是不可用的。

局部变量在函数返回或语句块结束时由系统收回内存空间。

3.静态局部变量

static修饰的局部变量生命周期和程序相同,并且只会被初始化一次

其作用域为局部,当定义它的函数或语句块结束时,其作用域随之结束

当程序想要使用全局变量的时候应该先考虑使用static(考虑到数据安全性)

4.注意事项
  • 全局变量和静态局部变量自动初始化为0
  • 局部变量不会自动初始化,其值是不确定的,程序中应该有初始化局部变量的代码,否则编译可能会报错(不同的编译器不一样)
  • 局部变量和全局变量的名称可以相同,在某函数或语句块内部,如果局部变量名与全局变量名相同,就会屏蔽全局变量而使用局部变量,如果想使用全局变量,可以在变量名前加两个冒号(::)
  • for循环初始化语句中定义的变量的作用域是for语句块

函数参数的传递

调用函数的时候,调用者把数值赋给了函数的参数

实参:调用者程序中书写的在函数名括号中的参数,可以是常量变量表达式

形参:函数的参数列表

在函数定义的代码中,修改形参的值,会不会影响实参。

示例:

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; // 指定缺省的命名空间。
// 调用函数的时候,调用者把数值赋给了函数的参数。
// 实参:调用者程序中书写的在函数名括号中的参数,可以是常量、变量和表达式。
// 形参:函数的参数列表。
// 在函数定义的代码中,修改形参的值,会不会影响实参?
void func(int no, string str); // 向超女表白的函数。
int main(){
int bh = 3; // 超女的编号。
string message = "我是一只傻傻鸟。"; // 向超女表白的内容。

// func(bh, message); // 调用向超女表白的函数。
// func(7, "小姐姐好漂亮哟。"); // 调用向超女表白的函数。
{
int no=7;
string str= "小姐姐好漂亮哟。";
no = 5; str = "我有一只小小鸟。";
cout << "亲爱的" << no << "号:" << str << endl;
}
cout << "亲爱的" << bh << "号:" << message << endl;
}
void func(int no, string str){ // 向超女表白的函数。
no = 5; str = "我有一只小小鸟。";
cout << "亲爱的" << no << "号:" << str << endl;
}

函数分文件编写

头文件(.h):需要包含的头文件,声明全局变量,函数的声明,数据结构和类的声明等

源文件(.cpp):函数的定义、类的定义

主程序:main函数,程序的核心流程,需要用#include "头文件名"把头文件包含进来

编译:

Windows是集成开发环境,不需要写编译指令。

在Linux系统下,把全部的源文件一起编译,如:g++ -o demo demo.cpp tools.cpp girls.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
26
27
28
29
30
31
32
33
34
35
36
/*demo01.cpp*/
#include "tools.h" // 包含头文件tools.h,min和max函数在里面。
#include "girls.h" // 包含头文件girls.h,print函数在里面。
int main(){
cout << "max(5,8)=" << max(5, 8) << endl;
cout << "min(5,8)=" << min(5, 8) << endl;
print(3, "我是一只傻傻鸟。");
}

/*girls.cpp*/
#include "girls.h"
void print(int no, string str){ // 表白神器
cout << "亲爱的" << no << "号:" << str << endl;
}

/*tools.cpp*/
#include "tools.h"
int max(int a, int b){ // 比较两个数的大小,返回较大者
return a > b ? a : b;
}
int min(int a, int b){ // 比较两个数的大小,返回较小者。
return a < b ? a : b;
}

/*girls.h*/
#pragma once
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间
void print(int no, string str); // 表白神器

/*toolss.h*/
#pragma once
#include <iostream> // 包含头文件
using namespace std; // 指定缺省的命名空间
int max(int a, int b); // 比较两个数的大小,返回较大者
int min(int a, int b); // 比较两个数的大小,返回较小者

VS中调试程序

F9设置/取消断点

F5/F10开始调试

Shift+F5放弃调试

F10逐过程执行

F11逐语句执行(可进入函数内部)

局部变量窗口显示了变量的值,也可以修改

递归函数

一个函数可以调用另一个函数,作为特例,如果函数调用了自己,就像故事中提到了同样的故事一样,我们把函数在运行时调用自己的情况叫做递归。

递归函数中一定要有递归终止的条件,否则是死递归。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int f(int x){ // 递归函数
if (x == 0) return 0; // 递归终止的条件
return x + f(x - 1); // 在函数体中调用了自己。
}
int main(){
cout << "f(100)=" << f(100) << endl;
// 100+99+98+....+1+0
// 嵌套的调用函数 进入函数的过程是递去 函数返回的过程是归来

// 计算从1加到100的和
int sum = 0; // 存放累加的值。
for (int ii = 1; ii <= 100; ii++)
sum = sum + ii;
cout << "sum=" << sum << endl;
}

sizeof运算符

sizeof运算符用于求数据类型或变量占用的内存空间

用于数据类型:sizeof(数据类型)

用于变量:sizeof(变量名)sizeof 变量名

注意:

  • 在32位和64位操作系统中,同一种数据类型占用的内存空间可能不一样
  • 字符串(string)不是C++的基本数据类型,用sizeof求它占用内存的大小没有意义

image-20240414195425331

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>         // 包含头文件
using namespace std; // 指定缺省的命名空间
int main(){
// 用于数据类型:sizeof(数据类型)
// 用于变量:sizeof(变量名) 或 sizeof 变量名
// C++常用的数据类型:整数(int)、浮点数(float和double)、字符(char)和布尔(bool)
cout << "sizeof(int)=" << sizeof(int) << endl;
cout << "sizeof(float)=" << sizeof(float) << endl;
cout << "sizeof(double)=" << sizeof(double) << endl;
cout << "sizeof(char)=" << sizeof(char) << endl;
cout << "sizeof(bool)=" << sizeof(bool) << endl;

int i; cout << "sizeof(int)=" << sizeof i << endl;
float f; cout << "sizeof(float)=" << sizeof f << endl;
double d; cout << "sizeof(double)=" << sizeof d << endl;
char c; cout << "sizeof(char)=" << sizeof c << endl;
bool b; cout << "sizeof(bool)=" << sizeof b << endl;
}

字符型的基本概念

字符型(char)占用的内存空间是1个字节,书写用单引号包含。

在内存中,不存放字符本身,而是存放与它对应的编码,即ASCII码。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是现今最通用的单字节编码方案,包含了33个控制字符(具有特殊含义无法显示的字符)和95个可显示字符。

'X' -> 88 01011000 'a'->97 01100001 '3'->51 00110011

1.ASCII 控制字符 (0~31)
十进制 符号 中文解释 十进制 符号 中文解释
0 NULL 空字符 16 DLE 数据链路转义
1 SOH 标题开始 17 DC1 设备控制 1
2 STX 正文开始 18 DC2 设备控制 2
3 ETX 正文结束 19 DC3 设备控制 3
4 EOT 传输结束 20 DC4 设备控制 4
5 ENQ 询问 21 NAK 拒绝接收
6 ACK 收到通知 22 SYN 同步空闲
7 BEL 23 ETB 传输块结束
8 BS 退格 24 CAN 取消
9 HT 水平制表符 25 EM 介质中断
10 LF 换行键 26 SUB 替换
11 VT 垂直制表符 27 ESC 换码符
12 FF 换页键 28 FS 文件分隔符
13 CR 回车键 29 GS 组分隔符
14 SO 移出 30 RS 记录分离符
15 SI 移入 31 US 单元分隔符
2.ASCII 可显示字符 (32~127)
十进制 符号 中文解释 十进制 符号 中文解释
32 空格 80 P 大写字母 P
33 ! 感叹号 81 Q 大写字母 Q
34 " 双引号 82 R 大写字母 R
35 # 井号 83 S 大写字母 S
36 $ 美元符 84 T 大写字母 T
37 % 百分号 85 U 大写字母 U
38 & 86 V 大写字母 V
39 ' 单引号 87 W 大写字母 W
40 ( 左括号 88 X 大写字母 X
41 ) 右括号 89 Y 大写字母 Y
42 * 星号 90 Z 大写字母 Z
43 + 加号 91 [ 左中括号
44 , 逗号 92 \ 斜线
45 - 减号 93 ] 右中括号
46 . 句点或小数点 94 ^ 音调符号
47 / 反斜线 95 _ 下划线
48 0 数字0的符号 96 ` 重音符
49 1 数字1的符号 97 a 小写字母 a
50 2 数字2的符号 98 b 小写字母 b
51 3 数字3的符号 99 c 小写字母 c
52 4 数字4的符号 100 d 小写字母 d
53 5 数字5的符号 101 e 小写字母 e
54 6 数字6的符号 102 f 小写字母 f
55 7 数字7的符号 103 g 小写字母 g
56 8 数字8的符号 104 h 小写字母 h
57 9 数字9的符号 105 i 小写字母 i
58 : 冒号 106 j 小写字母 j
59 ; 分号 107 k 小写字母 k
60 < 小于 108 l 小写字母 l
61 = 等号 109 m 小写字母 m
62 > 大于 110 n 小写字母 n
63 ? 问号 111 o 小写字母 o
64 @ 电子邮件符号 112 p 小写字母 p
65 A 大写字母 A 113 q 小写字母 q
66 B 大写字母 B 114 r 小写字母 r
67 C 大写字母 C 115 s 小写字母 s
68 D 大写字母 D 116 t 小写字母 t
69 E 大写字母 E 117 u 小写字母 u
70 F 大写字母 F 118 v 小写字母 v
71 G 大写字母 G 119 w 小写字母 w
72 H 大写字母 H 120 x 小写字母 x
73 I 大写字母 I 121 y 小写字母 y
74 J 大写字母 J 122 z 小写字母 z
75 K 大写字母 K 123 { 左大括号
76 L 大写字母 L 124 | 竖线
77 M 大写字母 M 125 } 右大括号
78 N 大写字母 N 126 ~ 波浪号
79 O 大写字母 O 127 删除
  • 32是空格
  • 48~57是0到9十个阿拉伯数字
  • 65~90是26个大写英文字母
  • 97~122号是26个小写英文字母
  • 其余的是一些标点符号、运算符号等
  • 第127个字符表示的是键盘上的删除键
3.字符的本质
  • 字符的本质是整数,取值范围是0~127
  • 在书写的时候可以用单引号包含,也可以用整数
  • 如果书写的时候用单引号包含,程序执行的时候,将把符号解释为对应的整数
  • 显示的时候,把整数解释为对应的符号,也可以直接显示整数
  • 可以与整数进行任何运算,运算的时候,书写方式可以用字符,也可以用整数
  • C++为什么没有提供1字节的整型?
  • 字符型也可以用unsigned修饰,意义何在?

示例:

1
2
3
4
5
6
7
8
9
10
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
// 字符型(char)占用的内存空间是1个字节,书写用单引号包含。
int a = 'X';
cout << "a=" << a << endl;
cout << "sizeof(a)=" << sizeof(a) << endl;
// 1234567 00000000 00010010 11010110 10000111
// 'X' -> 88 'a'->97 '3'->51
}

转义字符

在C++程序中,使用转义字符的原因有两个:

  • 控制字符没有符号,无法书写,只能用其它的符号代替
  • 某些符号已被C++征用,语义冲突,只能用其它的符号代替
ASCII码值 转义字符 含义
0 \0 空,给字符型变量赋值时可以直接书写0
10 \n 换行(LF) ,将当前位置移到下一行开头
13 \r 回车(CR) ,将当前位置移到本行开头
9 \t 水平制表(HT) (跳到下一个TAB位置)
92 \\ 斜线
34 \" 双引号,书写字符时不必转义
39 \' 单引号,书写字符串中不必转义
7 \a 警报
8 \b 退格(BS) ,将当前位置移到前一列
12 \f 换页(FF),将当前位置移到下页开头
11 \v 垂直制表(VT)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
// 字符型(char)占用的内存空间是1个字节,书写用单引号包含。
char a = '\'';
cout << "a=" << a << endl;

// 换行符 ASCII值是10 书写用\n
cout << "我是一'只傻傻鸟!\n";

// 水平制表符用于对齐输出的内容。
cout << "1\t西施\n";
cout << "100\t西瓜\n";
cout << "10000\t冰冰\n";
}

C++11的原始字面量

原始字面量(值)可以直接表示字符串的实际含义,不需要转义和连接。

语法:

1
2
R"(字符串的内容)"
R"xxx(字符串的内容)xxx"

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>         // 包含头文件
using namespace std; // 指定缺省的命名空间
int main(){
// 使用转义的方法
string path = "C:\\Program Files\\Microsoft OneDrive\\tail\\nation";
cout << "path is: " << path << endl;
// 使用C++11原始字面量
string path1 = R"abcd(C:\Program Files\Microsoft OneDrive\tail\nation)abcd";
cout << "path1 is: " << path1 << endl;
string str = R"(
<no>0001</no>
<name>西施</name>
<sc>火树银花</sc>
<yz>沉鱼</yz>
<age>23</age>
<weight>48.5</weight>
<height>170</height>)";
cout << str << endl;
}

字符串型

C++风格字符串:string 变量名="字符串的内容";

C风格字符串:char 变量名[]="字符串的内容";

C风格字符串的本质是字符数组,C++风格字符串的本质是,它封装了C风格字符串。

C++风格字符串的常用操作:

  • 赋值:变量名="字符串的内容"

  • 拼接:变量名=变量名+"字符串的内容一"+"字符串的内容一"+......+"字符串的内容n"

    如果字符串的内容都是常量,不要写加号(+),如果内容很长,可以分成多行书写。

  • 比较:支持==!=><关系运算符,常用的是==!=

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
string str="西施"; // 声明字符串变量并初始化。
str = "美女西施"; // 对字符串变量重新赋值。
cout << "请输入超女姓名:"; // 输出提示文字内容。
cin >> str; // 从控制台输入数据,保存在变量str中。
if (str == "冰冰") cout << "我喜欢\n"; // 用==可以判断两个字符串是否相同。
if (str != "冰冰") cout << "我不玩了\n"; // 用!=可以判断两个字符串是否不相同。
str = "姓名:" + str + ",这是我现女友。"; // 用+可以拼接多个字符串。
// 如果字符串的内容都是常量,不要写加号(+),如果内容很长,可以分成多行书写。
str = "超女姓名:"
"幂幂"
",这是我的前女友。";
cout << str << endl;
}

布尔型

在C和C++中,关系运算和逻辑运算的结果有两种:真和假。

C语言用0表示假,非0表示真。

为了提高代码的可读性,C++新增了 bool 类型,占用1字节的内存,用true表示真,false表示假。

bool类型本质上是1字节的整数(unsigned char),取值只有1和0。

在程序中,书写的时候可以用true和false,编译器把它们解释为1和0。

如果对bool型变量赋非0的值,将转换成1。

用cin输入和cout输出的时候,仍是1和0,不会被解释为true和false。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
bool b = 1;
cout << "b+b=" << b+b << endl; // 将显示2
// 如果对bool型变量赋非0的值,将转换成1。
b = 30;
cout << "b=" << b << endl; // 将显示1
// 找到布尔变量b的内存,把里面的数据强制为8。
char* c = (char *) & b;
*c = 8;
cout << "b=" << b << endl; // 将显示8
}

数据类型的转换

计算机进行运算时,要求各操作数的类型具有相同的大小和存储方式

在实际开发中,不同类型的数据进行混合运算是基本需求

自动类型转换:某些类型的转换编译器可以隐式的进行,不需程序员干预

强制类型转换:有些类型的转换需要程序员显式指定

1.自动类型转换

不同数据类型的差别在于取值范围和精度,数据的取值范围越大,精度越高。

整型从低到高:

char -> short -> int -> long -> long long

浮点型从低到高:

float -> double -> long double

自动类型转换的规则如下:

  • 如果一个表达式中出现了不同类型操作数的混合运算,较低类型将自动向较高类型转换
  • 当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型
  • 赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型
  • 赋值运算右值超出了左值类型的表示范围,把该右值截断后赋给左值,所得结果可能毫无意义。
2.强制类型转换

为了让程序设计更灵活,转换的目的更清晰,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; // 指定缺省的命名空间。
int main(){
char a = 30;
int b = 102400;
long long c = 15000000000001;

// 如果一个表达式中出现了不同类型操作数的混合运算,较低类型将自动向较高类型转换。
cout << "a+b+c=" << a + b + c << endl;

// 当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型。
cout << "8/5=" << ((double)8) / 5 << endl;

// 赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型。
// 赋值运算右值超出了左值类型的表示范围,把该右值截断后赋给左值,所得结果可能毫无意义。
int d = (int)23.59; // 降低了精度。
cout << "d=" << d << endl;

unsigned int e = (unsigned int)4294967295+10; // 值被截断,从高位截断
cout << "e=" << e << endl;
// 4294967295 11111111111111111111111111111111
// 4294967296 000100000000000000000000000000000000
// 4294967297 000100000000000000000000000000000001
}

数据类型的别名typedef

创建数据类型的别名有两个目的:

  • 为名称复杂的类型创建别名,方便书写和记忆
  • 创建与平台无关的数据类型,提高程序的兼容性。

语法:typedef 原数据类型名 别名;

C++11还可以用using关键字创建数据类型的别名。

语法:using 别名=原数据类型名;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
// 1)为名称复杂的类型创建别名,方便书写和记忆。
// 2)创建与平台无关的数据类型,提高程序的兼容性。
// 在VS中,short是两个字节,int是四个字节,long也是四个字节,long long是八个字节。
typedef short int16_t; // 16位的整数。
typedef int int32_t; // 32位的整数。
typedef long long int64_t; // 64位的整数。
// 在Linux中,short是两个字节,int是四个字节,long也是八个字节,long long也是八个字节。
typedef short int16_t; // 16位的整数。
typedef int int32_t; // 32位的整数。
typedef long int64_t; // 64位的整数。

// 在程序源代码中,只使用别名int16_t、int32_t、int64_t,不使用原名。
}

指针的基本概念

1.变量的地址

变量是内存变量的简称,在C++中,每定义一个变量,系统就会给变量分配一块内存,内存是有地址的

image-20240415173324664

C++用运算符&获取变量在内存中的起始地址

语法:&变量名

2.指针变量

指针变量简称指针,它是一种特殊的变量,专用于存放变量在内存中的起始地址

语法:数据类型 *变量名;

数据类型必须是合法的C++数据类型(intchardouble或其它自定义的数据类型)。

星号*与乘法中使用的星号是相同的,但是,在这个场景中,星号用于表示这个变量是指针。

3.对指针赋值

不管是整型(int)、浮点型(float/double)、字符型(char),还是其它的数据类型的变量,它的地址都是一个十六进制数。我们用整型指针存放整数型变量的地址;用字符型指针存放字符型变量的地址;用浮点型指针存放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址。

语法:指针=&变量名;

注意:

  • 对指针的赋值操作也通俗的被称为“指向某变量”,被指向的变量的数据类型称为“基类型”
  • 如果指针的数据类型与基类型不符,编译会出现警告。但是,可以强制转换它们的类型
4.指针占用的内存

指针也是变量,是变量就要占用内存空间。

在64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节

在C++中,指针是复合数据类型,复合数据类型是指基于其它类型而定义的数据类型,在程序中,int是整型类型,int*是整型指针类型,int*可以用于声明变量,可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int*当成一种数据类型就是了。

使用指针

声明指针变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。

指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)

*运算符被称为间接值解除引用(解引用)运算符,将它用于指针,可以得到该地址的内存中存储的值

*也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。

变量和指向变量的指针就像同一枚硬币的两面

image-20240415180532366

哪个银行? 什么东西? 数额

程序在存储数据的时候,必须跟踪三种基本属性:

  • 数据存储在哪里
  • 数据是什么类型
  • 数据的值是多少

用两种策略可以达到以上目的:

  • 声明一个普通变量,声明时指出数据类型和变量名(符号名),系统在内部跟踪该内存单元
  • 声明一个指针变量,存储的值是地址,而不是值本身,程序直接访问该内存单元

指针用于函数的参数

如果把函数的形参声明为指针,调用的时候把实参的地址传进去,形参中存放的是实参的地址,在函数中通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递传地址

值传递:函数的形参是普通变量。

传地址的意义如下:

  • 可以在函数中修改实参的值
  • 减少内存拷贝,提升性能

示例:

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; // 指定缺省的命名空间
void func(int *num, string *str) {
cout << "号码:" << *num << "号:" << *str << endl;
}
void func2(int a, int b, int c, int *max, int *min) {
*max = a > b ? a : b;
*min = a < b ? a : b;
*max = *max > c ? *max : c;
*min = *min < c ? *min : c;
}
int main() {
int num = 3;
string str = "我是一只傻傻鸟";
int* pnum = &num;
string* pstr = &str;
func(&num, &str);
*pnum = 8;
*pstr = "我有一只小小鸟";
cout << "号码:" << *pnum << "号:" << *pstr << endl;

int a = 180, b = 170, c = 155, m, n;
func2(a, b, c, &m, &n);
cout << "最高:" << m << "最矮:" << n << endl;

}

用const修饰指针

1.常量指针

语法:const 数据类型 *变量名;

不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)。

注意:

  • 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)
  • 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值
  • 如果用于形参,虽然指向的对象可以改变,但这么做没有任何意义
  • 如果形参的值不需要改变,建议加上const修饰,程序可读性更好
2.指针常量

语法:数据类型 * const 变量名;

指向的变量(对象)不可改变。

注意:

  • 在定义的同时必须初始化,否则没有意义
  • 可以通过解引用的方法修改内存地址中的值
  • C++编译器把指针常量做了一些特别的处理,改头换面之后,有一个新的名字,叫引用
3.常指针常量

语法:const 数据类型 * const 变量名;

指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值。

常引用。

常量指针:指针指向可以改,指针指向的值不可以更改。

指针常量:指针指向不可以改,指针指向的值可以更改。

常指针常量:指针指向不可以改,指针指向的值不可以更改。

记忆秘诀: ==*表示指针,指针在前先读指针;指针在前指针就不允许改变==

常量指针:const 数据类型 *变量名

指针常量:数据类型 * const 变量名

void关键字

在C++中,void表示为无类型,主要有三个用途:

1.函数的返回值用void,表示函数没有返回值

1
2
3
4
void func(int a,int b){
// 函数体代码。
return;
}

2.函数的参数填void,表示函数不需要参数(或者让参数列表空着)

1
2
3
4
int func(void){
// 函数体代码。
return 0;
}

3.函数的形参用void *,表示接受任意数据类型的指针

注意:

  • 不能用void声明变量,它不能代表一个真实的变量,但是,用void *可以
  • 不能对void *指针直接解引用(需要转换成其它类型的指针)*
  • *把其它类型的指针赋值给void*指针不需要转换
  • void *指针赋值给把其它类型的指针需要转换

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>         // 包含头文件
using namespace std; // 指定缺省的命名空间
void convert(string name, void *p) {
cout << name << "的地址:" << p << endl;
}
int main() {
int a = 3;
char b;
cout << "a的地址:" << (void*) & a << endl;
cout << "b的地址:" << (void*) & b << endl;

convert("a", &a);
convert("b", &b);
}

C++内存模型

在 C++ 中,程序运行时,内存主要分成四个区,分别是栈、堆、数据段代码段

image-20240415182016842

栈:存储局部变量、函数参数和返回值。

堆:存储动态开辟内存的变量。

数据段:存储全局变量和静态变量。

代码段:存储可执行程序的代码和常量(例如字符常量),此存储区不可修改。

栈和堆的主要区别:

  • 管理方式不同:栈是系统自动管理的,在出作用域时,将自动被释放;堆需手动释放,若程序中不释放,程序结束时由操作系统回收
  • 空间大小不同:堆内存的大小受限于物理内存空间;而栈就小得可怜,一般只有8M(可以修改系统参数)
  • 分配方式不同:堆是动态分配;栈有静态分配和动态分配(都是自动释放)
  • 分配效率不同:栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专门的指令,效率比较高;堆是由C++函数库提供的
  • 是否产生碎片:对于栈来说,进栈和出栈都有着严格的顺序(先进后出),不会产生碎片;而堆频繁的分配和释放,会造成内存空间的不连续,容易产生碎片,太多的碎片会导致性能的下降
  • 增长方向不同:栈向下增长,以降序分配内存地址;堆向上增长,以升序分配内存地址。

动态分配内存new和delete

使用堆区的内存有四个步骤:

1)声明一个指针;

2)用new运算符向系统申请一块内存,让指针指向这块内存;

3)通过对指针解引用的方法,像使用变量一样使用这块内存;

4)如果这块内存不用了,用delete运算符释放它

申请内存的语法:

1
new 数据类型(初始值);  // C++11支持{}

如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)

释放内存的语法:delete 地址;

释放内存不会失败(还钱不会失败)。

注意:

  • 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据
  • 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存
  • 动态分配的内存生命周期与程序相同,程序退出时,如果没有释放,系统将自动回收
  • 就算指针的作用域已失效,所指向的内存也不会释放
  • 用指针跟踪已分配的内存时,不能跟丢

示例:

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; // 指定缺省的命名空间。
int main(){
// 1)声明一个指针;
// 2)用new运算符向系统申请一块内存,让指针指向这块内存;
// 3)通过对指针解引用的方法,像使用变量一样使用这块内存;
// 4)如果这块内存不用了,用delete运算符释放它。
// 申请内存的语法:new 数据类型(初始值); // C++11支持{}
// 释放内存的语法:delete 地址;
int* p = new int(5);
cout << "*p=" << *p << endl;
*p = 8;
cout << "*p=" << *p << endl;
delete p;

/* for (int ii = 1; ii > 0; ii++){
int* p = new int[100000]; // 一次申请100000个整数,这个语法以后再讲。
cout << "ii="<<ii<<",p=" << p << endl;
}*/
}

二级指针

指针指针变量的简称,也是变量,是变量就有地址

指针用于存放普通变量地址

二级指针用于存放指针变量地址

声明二级指针的语法:数据类型** 指针名;

使用指针有两个目的:

  • 传递地址;
  • 存放动态分配的内存的地址

在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参用二级指针。

把普通变量的地址传入函数后可以在函数中修改变量的值;把指针的地址传入函数后可以在函数中指针的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

void func(int** pp){
*pp = new int(3);
cout << "pp=" << pp << ",*pp=" << *pp << endl;
}

int main(){
int a = 8;
cout << "a=" << a << ",a的地址为:" << &a << endl;
int* pa = &a;
cout << "pa=" << pa << ",pa的地址为:" << &pa << ", *pa=" << *pa <<endl;
int** ppa = &pa;
cout << "pa的地址为:" << &pa << ",ppa=" << ppa << ", *ppa=" << *ppa;
}

空指针

在C和C++中,用0或NULL都可以表示空指针。

声明指针后,在赋值之前,让它指向空,表示没有指向任何地址。

1.使用空指针的后果

如果对空指针解引用,程序会崩溃。

如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常。所以,内存被释放后,也应该把指针指向空。

在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。

为什么空指针访问会出现异常?

NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。

2)C++11的nullptr

用0和NULL表示空指针会产生歧义,C++11建议用nullptr表示空指针,也就是(void *)0。

NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整形的情况下,会出现上述的问题。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议用nullptr替代NULL吧,而NULL就当做0使用。

注意:在Linux平台下,如果使用nullptr,编译需要加-std=c++11参数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

void func(int* no, string* str) // 向超女表白的函数。
{
if ((no == 0) || (str == 0)) return;

cout << "亲爱的" << *no << "号:" << *str << endl;
}

int main(){
// int bh = 3; // 超女的编号。
// string message = "我是一只傻傻鸟。"; // 向超女表白的内容。
int* bh = 0; // new int(3);
string* message = 0; // new string("我是一只傻傻鸟。");
func(bh,message); // 调用向超女表白的函数。
delete bh; delete message;
}

函数指针

函数的二进制代码存放在内存四区中的代码段,函数的地址是它在内存中的起始地址。如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其它函数。

使用函数指针的三个步骤:

  • 声明函数指针;
  • 让函数指针指向函数的地址;
  • 通过函数指针调用函数
1)声明函数指针

声明普通指针时,必须提供指针的类型。同样,声明函数指针时,也必须提供函数类型,函数的类型是指返回值参数列表(函数名和形参名不是)

假设函数的原型是:

1
2
3
4
5
6
7
int func1(int bh,string str);
int func2(int no,string message);
int func3(int id,string info);

bool func4(int id,string info);

bool func5(int id);

则函数指针的声明是:

1
2
3
int  (*pfa)(int,string);
bool (*pfb)(int,string);
bool (*pfc)(int);

pfa、pfb、pfc是函数指针名,必须用括号,否则就成了返回指针的函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func(int num, string str){
cout << "编号:" << num << ", " << str << endl;
}
int main(){
int num = 3;
string str = "yigvavava";
func(num, str);
void (*pfunc)(int, string);
pfunc = func;
pfunc(num, str);
(*pfunc)(num, str);
}
2)函数指针的赋值

函数名就是函数的地址

函数指针的赋值:函数指针名=函数名;

3)函数指针的调用
1
2
(*函数指针名)(实参);
函数指针名(实参);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void zs() {
cout << "采用方案一" << endl;
}
void ls() {
cout << "采用方案二" << endl;
}
void show(void (*pf)()) {
cout << "前期准备已完成." << endl;
pf();
cout << "收尾工作已完成." << endl;
}
int main(){
show(ls);
}

数组

一维数组的基本概念

数组是一组数据类型相同的变量,可以存放一组数据。

1.创建数组

声明数组的语法:数据类型 数组名[数组长度];

注意:数组长度必须是整数,可以是常量,也可以是变量和表达式

2.数组的使用

可以通过下标访问数组中元素,数组下标从0开始。

数组中每个元素的特征和使用方法与单个变量完全相同。

语法:数组名[数组下标]

注意:

  • 数组下标也必须是整数,可以是常量,也可以是变量
  • 合法的数组下标取值是:0~(数组长度-1)
3.数组占用内存的情况

数组在内存中占用的空间是连续的。

sizeof(数组名)可以得到整个数组占用内存空间的大小(只适用于C++基本数据类型)

4.数组的初始化

声明的时候初始化:

1
2
3
4
数据类型 数组名[数组长度] = { 值1,值2,值3, ...... , 值n};
数据类型 数组名[ ] = { 值1,值2,值3, ...... , 值n};
数据类型 数组名[数组长度] = { 0 }; // 把全部的元素初始化为0
数据类型 数组名[数组长度] = { }; // 把全部的元素初始化为0

注意:如果{}内不足数组长度个数据,剩余数据用0补全,但是,不建议这么用,你可能在数组中漏了某个值。如果想把数组中全部的元素初始化为0,可以在{}内只填一个0或什么也不填。

C++11标准可以不写等于号。

5.清空数组

memset()函数可以把数组中全部的元素清零。(只适用于C++基本数据类型)

函数原型:void *memset(void *s, int c, size_t n);

注意,在Linux下,使用memcpy()函数需要包含头文件#include <string.h>

6.复制数组

memcpy()函数可以把数组中全部的元素复制到另一个相同大小的数组。(只适用于C++基本数据类型)

函数原型:void *memcpy(void *dest, const void *src, size_t n);

注意,在Linux下,使用memcpy()函数需要包含头文件#include <string.h>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
int bh[] = {3, 6, 1,6,7,4,3,5,6,7,8,322,2,3,9}; // 超女编号。
string name[3]; // 超女姓名。
for (int ii = 0; ii < sizeof(bh)/sizeof(int); ii++){
cout << "bh["<<ii<<"]=" << bh[ii] << endl;
}
int bh1[sizeof(bh) / sizeof(int)]; // 数组长度必须是整数,可以是常量,也可以是变量和表达式。
memcpy(bh1, bh, sizeof(bh)); // 把数组bh中的内容复制到bh1。
for (int ii = 0; ii < sizeof(bh1) / sizeof(int); ii++){
cout << "bh1[" << ii << "]=" << bh1[ii] << endl;
}
}

一维数组和指针

1.指针的算术

将一个整型变量加1后,其值将增加1。

但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数

2.数组的地址
  • 数组在内存中占用的空间是连续的
  • C++将数组名解释为数组第0个元素的地址
  • 数组第0个元素的地址和数组首地址的取值是相同的
  • 数组第n个元素的地址是:数组首地址+n
  • C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)
3.数组的本质

数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。

4.数组名不一定会被解释为地址

在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。

可以修改指针的值,但数组名是常量,不可修改。

示例:

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; // 指定缺省的命名空间。
int main(){
char a; cout << "sizeof(char)=" << sizeof(char) << endl; // 1字节
short b; cout << "sizeof(short)=" << sizeof(short) << endl; // 2字节
int c; cout << "sizeof(int)=" << sizeof(int) << endl; // 4字节
double d; cout << "sizeof(double)=" << sizeof(double) << endl; // 8字节

cout << "a的地址是:" << (void *)& a << endl;
cout << "a的地址+1是:" << (void*)( & a + 1) << endl;

cout << "b的地址是:" << (void*)&b << endl;
cout << "b的地址+1是:" << (void*)(&b + 1) << endl;

cout << "c的地址是:" << (void*)&c << endl;
cout << "c的地址+1是:" << (void*)(&c + 1) << endl;

cout << "d的地址是:" << (void*)&d << endl;
cout << "d的地址+1是:" << (void*)(&d + 1) << endl;
}
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; // 指定缺省的命名空间。
int main(){
double a[5];
cout << "a的值是:" << (long long) a << endl;
cout << "&a的值是:" << (long long)&a << endl;

cout << "a[0]的地址是:" << (long long) &a[0] << endl;
cout << "a[1]的地址是:" << (long long) &a[1] << endl;
cout << "a[2]的地址是:" << (long long) &a[2] << endl;
cout << "a[3]的地址是:" << (long long) &a[3] << endl;
cout << "a[4]的地址是:" << (long long) &a[4] << endl;

double* p = a;
cout << "p的值是:" << (long long)p << endl;
cout << "p+0的值是:" << (long long)(p+ 0) << endl;
cout << "p+1的值是:" << (long long)(p + 1) << endl;
cout << "p+2的值是:" << (long long)(p + 2) << endl;
cout << "p+3的值是:" << (long long)(p + 3) << endl;
cout << "p+4的值是:" << (long long)(p + 4) << endl;
}
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; // 指定缺省的命名空间。

int main(){
int a[5] = { 3 , 6 , 5 , 8 , 9 };

// 用数组表示法操作数组。
cout << "a[0]的值是:" << a[0] << endl;
cout << "a[1]的值是:" << a[1] << endl;
cout << "a[2]的值是:" << a[2] << endl;
cout << "a[3]的值是:" << a[3] << endl;
cout << "a[4]的值是:" << a[4] << endl;

// 用指针表示法操作数组。
int* p = a;
cout << "*(p+0)的值是:" << *(p + 0) << endl;
cout << "*(p+1)的值是:" << *(p + 1) << endl;
cout << "*(p+2)的值是:" << *(p + 2) << endl;
cout << "*(p+3)的值是:" << *(p + 3) << endl;
cout << "*(p+4)的值是:" << *(p + 4) << endl;
}

一维数组用于函数的参数

1.指针的数组表示

在C++内部,用指针来处理数组

C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)

C++编译器把 地址[下标] 解释为 *(地址+下标)

2.一维数组用于函数的参数

一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。

书写方法有两种:

1
2
void func(int* arr, int len);
void func(int arr[], int len);

注意:

在函数中,可以用数组表示法,也可以用指针表示法。

在函数中,不要对指针名用sizeof运算符,它不是数组名。

image-20240415201800031

示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
char a[20]; // 这是一个长度为20的字符型数组。
int* p = (int *)a; // 让整型指针p指向数组a的内存。
for (int ii = 0; ii < 6; ii++){
p[ii] = ii + 300; // 用数组表示法操作指针。
}
for (int ii = 0; ii < 6; ii++){
cout << "*(p+" << ii << ")的值是:" << *(p + ii) << endl; // 地址[下标] 解释为 *(地址+下标)。
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// void func(int *arr,int len)
void func(int arr[],int len){
for (int ii = 0; ii < len; ii++){
cout << "arr[" << ii << "]的值是:" << arr[ii] << endl; // 用数组表示法操作指针。
cout << "*(arr+" << ii << ")的值是:" << *(arr + ii) << endl; // 地址[下标] 解释为 *(地址+下标)。
}
}
int main(){
int a[] = {2,8,4,6,7,1,9};
func(a, sizeof(a) / sizeof(int));
}

用new动态创建一维数组

普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。

动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度];

释放一维数组的语法:delete [] 指针;

注意:

  • 动态创建的数组没有数组名,不能用sizeof运算符
  • 可以用数组表示法和指针表示法两种方式使用动态创建的数组
  • 必须使用delete[]来释放动态数组的内存(不能只用delete)
  • 不要用delete[]来释放不是new[]分配的内存
  • 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)
  • 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)
  • 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放
  • 如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常
  • 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存

示例:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main(){
int *arr=new int[8]; // 创建8个元素的整型数组。

for (int ii = 0; ii < 8; ii++){
arr[ii] = 100 + ii; // 数组表示法
cout << "arr[" << ii << "]=" << *(arr + ii) << endl; // 指针表示法。
}
delete[]arr;
}

各种形参的使用场景

传值、传地址和传引用的指导原则《C++ Primer Plus》

1.如果不需要在函数中修改实参

  • 如果实参很小,如C++内置的数据类型或小型结构体,则按值传递
  • 如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)
  • 如果实参是较大的结构,则使用const指针或const引用
  • 如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。

2.如果需要在函数中修改实参

  • 如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x
  • 如果实参是数组,则只能使用指针
  • 如果实参是结构体,则使用指针或引用
  • 如果实参是类,则使用引用。

当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。

例如:对于基本类型,cin使用引用,因此可以使用cin>>a,而不是cin>>&a

引用

引用的基本概念

引用变量是C++新增的复合类型

引用是已定义的变量的别名

引用的主要用途是用作函数的形参和返回值

声明/创建引用的语法:数据类型 &引用名=原变量名;

注意:

  • 引用的数据类型要与原变量名的数据类型相同
  • 引用名和原变量名可以互换,它们值和内存单元是相同的
  • 必须在声明引用的时候初始化,初始化后不可改变
  • C和C++用&符号来指示/取变量的地址,C++给&符号赋予了另一种含义

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
int main() {
int a = 3;
int& ra = a;
cout << "a的地址:" << &a << ", a的值为:" << a << endl;
cout << "ra的地址" << &ra << ", ra的值为:" << ra << endl;
ra = 8;
cout << "a的地址:" << &a << ", a的值为:" << a << endl;
cout << "ra的地址" << &ra << ", ra的值为:" << ra << endl;
int b = 5;
ra = b;
cout << "b的地址" << &b << ", b的值为:" << b << endl;
cout << "a的地址:" << &a << ", a的值为:" << a << endl;
cout << "ra的地址" << &ra << ", ra的值为:" << ra << endl;
}

引用的本质

引用是指针常量的伪装

引用是编译器提供的一个有用且安全的工具,去除了指针的一些缺点,禁止了部分不安全的操作

变量是什么?变量就是一个在程序执行过程中可以改变的量

换一个角度,变量是一块内存区域的名字,它代表了这块内存区域,当我们对变量进行修改的时候,会引起内存区域中内容的改变。

在计算机看来,内存区域根本就不存在什么名字,它仅有的标志就是它的地址,因此我们若想修改一块内存区域的内容,只有知道他的地址才能实现。

所谓的变量只不过是编译器给我们进行的一种抽象,让我们不必去了解更多的细节,降低我们的思维跨度而已

程序员拥有引用,但编译器仅拥有指针(地址)

引用的底层机制实际上是和指针一样的。不要相信有别名,不要认为引用可以节省一个指针的空间,因为这一切不会发生,编译器还是会把引用解释为指针

引用和指针本质上没有区别

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
int main() {
int a = 3; // 声明普通的整型变量
int& ra = a; // 创建引用ra,ra是a的别名
int* const rb = &a; // 常见指针常量rb,让它指向变量a
cout << "a的地址:" << &a << ", a的值为:" << a << endl;
cout << "ra的地址" << &ra << ", ra的值为:" << ra << endl;
cout << "rb:" << rb << ", rb指向的值为:" << *rb << endl;
ra = 8;
cout << "a的地址:" << &a << ", a的值为:" << a << endl;
cout << "ra的地址" << &ra << ", ra的值为:" << ra << endl;

int b = 5;
ra = b;
cout << "b的地址" << &b << ", b的值为:" << b << endl;
cout << "a的地址:" << &a << ", a的值为:" << a << endl;
cout << "ra的地址" << &ra << ", ra的值为:" << ra << endl;
}

引用用于函数的参数

把函数的形参声明为引用,调用函数的时候,形参将成为实参的别名

这种方法也叫按引用传递或传引用。(传值、传地址、传引用只是说法不同,其实都是传值。)

引用的本质是指针,传递的是变量的地址,在函数中,修改形参会影响实参。

1)传引用的代码更简洁。

2)传引用不必使用二级指针。

3)引用的属性和特别之处。

示例:

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; // 指定缺省的命名空间。

void func1(int no, string str){ // 传值
no = 8;
str = "我有一只小小鸟。";
cout << "亲爱的" << no << "号:" << str << endl;
}
void func2(int* no, string* str) // 传地址
*no = 8;
*str = "我有一只小小鸟。";
cout << "亲爱的" << *no << "号:" << *str << endl;
}
void func3(int &no, string &str){ // 传引用
no = 8;
str = "我有一只小小鸟。";
cout << "亲爱的" << no << "号:" << str << endl;
}
int main(){
int bh = 3; // 超女的编号。
string message = "我是一只傻傻鸟。"; // 向超女表白的内容
//func1(bh, message); // 传值
//func2(&bh, &message); // 传地址
func3(bh, message); // 传引
cout << "亲爱的" << bh << "号:" << message << 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
27
28
29
30
#include <iostream>         // 包含头文件
using namespace std; // 指定缺省的命名空间
struct st_girl{ // 定义超女结构体
int no; // 超女编号。
string str; // 表白内容。
};
void func1(st_girl girl){ // 传值。
girl.no = 8;
girl.str = "我有一只小小鸟。";
cout << "亲爱的" << girl.no << "号:" << girl.str << endl;
}

void func2(st_girl *girl){ // 传地址
girl->no = 8;
girl->str = "我有一只小小鸟。";
cout << "亲爱的" << girl->no << "号:" << girl->str << endl;
}
void func3(st_girl & girl) // 传引用。
{
girl.no = 8;
girl.str = "我有一只小小鸟。";
cout << "亲爱的" << girl.no << "号:" << girl.str << endl;
}
int main(){
st_girl girl = { 3,"我是一只傻傻鸟。" };
func1(girl); // 传值。
//func2(&girl); // 传地址。
//func3(girl); // 传引用。
cout << "亲爱的" << girl.no << "号:" << girl.str << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func1(int** p){ // 传地址,实参是指针的地址,形参是二级指针
*p = new int(3); // p是二级指针,存放指针的地址。
cout << "func1内存的地址是:" << *p << ",内存中的值是:" << **p << endl;
}
void func2(int*& p){ // 传引用,实参是指针,形参是指针的别名
p = new int(3); // p是指针的别名。
cout << "func2内存的地址是:" << p << ",内存中的值是:" << *p << endl;
}
int main(){
int* p = nullptr; // 存放在子函数中动态分配内存的地址。
func1(&p); // 传地址,实参填指针p的地址。
//func2(p); // 传引用,实参填指针p。
cout << "main 内存的地址是:" << p << ",内存中的值是:" << *p << endl;
delete p;
}

引用的形参和const

如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量,让引用指向临时变量

什么时候将创建临时变量呢?

  • 引用是const
  • 数据对象的类型是正确的,但不是左值
  • 数据对象的类型不正确,但可以转换为正确的类型。

结论:如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。

将引用形参声明为const的理由有三个:

  • 使用const可以避免无意中修改数据的编程错误
  • 使用const使函数能够处理const和非const实参,否则将只能接受非const实参
  • 使用const,函数能正确生成并使用临时变量

左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针

非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式

示例:

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;
void func1(int no, string str){ // 传值
cout << "亲爱的" << no << "号:" << str << endl;
}
void func2(const int* no,const string* str){ // 传地址
cout << "亲爱的" << *no << "号:" << *str << endl;
}
void func3(const int& no, const string& str){ // 传引用
cout << "亲爱的" << no << "号:" << str << endl;
}
int main(){
//int bh = 3; // 超女的编号。
//string message = "我是一只傻傻鸟。"; // 向超女表白的内容。
//
func1(8, "我是一只小小鸟。");
// func2(8, "我是一只小小鸟。");
func3('X', "我是一只小小鸟。");
////func1(bh, message); // 传值。
////func2(&bh, &message); // 传地址。
////func3(bh, message); // 传引用。
//cout << "亲爱的" << bh << "号:" << message << endl;
}

引用用于函数的返回值

传统的函数返回机制与值传递类似。

函数的返回值被拷贝到一个临时位置(寄存器或栈),然后调用者程序再使用这个值。

double m=sqrt(36); // sqrt()是求平方根函数。

sqrt(36)的返回值6被拷贝到临时的位置,然后赋值给m。

cout << sqrt(25);

sqrt(25)的返回值5被拷贝到临时的位置,然后传递给cout。

如果返回的是一个结构体,将把整个结构体拷贝到临时的位置。

如果返回引用不会拷贝内存。

语法:

返回值的数据类型& 函数名(形参列表);

注意:

  • 如果返回局部变量的引用,其本质是野指针,后果不可预知
  • 可以返回函数的引用形参、类的成员、全局变量、静态变量
  • 返回引用的函数是被引用的变量的别名,将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; // 指定缺省的命名空间
const int &func2(int &ra){ // 返回的是引用
ra++;
cout << "ra的地址是:" << &ra << ",ra=" << ra << endl;
return ra;
}
int main(){
int a = 3;
const int& b = func2(a); // 返回的是引用。

cout << " a的地址是:" << &a << ", a=" << a << endl;
cout << " b的地址是:" << &b << ", b=" << b << endl;

// func2(a) = 10; // 返回引有的函数是被引用的变量的别名。

// cout << " a的地址是:" << &a << ", a=" << a << endl;
// cout << " b的地址是:" << &b << ", b=" << b << endl;
}

各种形参的使用场景

传值、传地址和传引用的指导原则《C++ Primer Plus》

1.如果不需要在函数中修改实参
  • 如果实参很小,如C++内置的数据类型或小型结构体,则按值传递
  • 如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)
  • 如果实参是较大的结构,则使用const指针或const引用
  • 如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)
2.如果需要在函数中修改实参
  • 如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x
  • 如果实参是数组,则只能使用指针
  • 如果实参是结构体,则使用指针或引用
  • 如果实参是类,则使用引用

当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。

例如:对于基本类型,cin使用引用,因此可以使用cin>>a,而不是cin>>&a

函数的默认参数

默认参数是指调用函数的时候,如果不书写实参,那么将使用的一个缺省值

语法:

1
返回值 函数名(数据类型 参数=值, 数据类型 参数=值,……);

注意:

  • 如果函数的声明和定义是分开书写的,在函数声明中书写默认参数,函数的定义中不能书写默认参数
  • 函数必须从右到左设置默认参数。也就是说,如果要为某个参数设置默认值,则必须为它后面所有的参数设置默认值
  • 调用函数的时候,如果指定了某个参数的值,那么该参数前面所有的参数都必须指定。

示例:

1
2
3
4
5
6
7
8
9
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func(int bh, const string& name = "西施", const string& message = "我喜欢你。"){ // 向超女表白的函数。
cout << "亲爱的" << name << "(" << bh << "):" << message << endl;
}
int main(){
func(3, "冰冰", "我是一只傻傻鸟。");
func(5);
}

函数重载

函数重载(函数多态)是指设计一系列同名函数,让它们完成相同(似)的工作。

C++允许定义名称相同的函数,条件是它们的特征(形参的个数、数据类型和排列顺序)不同。

1
2
3
4
5
int func(short a ,string b);
int func(int a ,string b);
int func(double a,string b);
int func(int a ,string b, int len);
int func(string b , int a);

调用重载函数的时候,在代码中我们用相同的函数名,但是,后面的实参不一样,编译器根据实参与重载函数的形参进行匹配,然后决定调用具体的函数,如果匹配失败,编译器将视为错误。

在实际开发中,视需求重载各种数据类型,不要重载功能不同的函数。

注意:

  • 使用重载函数时,如果数据类型不匹配,C++尝试使用类型转换与形参进行匹配,如果转换后有多个函数能匹配上,编译将报错
  • 引用可以作为函数重载的条件,但是,调用重载函数的时候,如果实参是变量,编译器将形参类型的本身和类型引用视为同一特征
  • 如果重载函数有默认参数,调用函数时,可能导致匹配失败
  • const不能作为函数重载的特征
  • 返回值的数据类型不同不能作为函数重载的特征
  • C++的名称修饰:编译时,对每个函数名进行加密,替换成不同名的函数。
1
2
3
4
void MyFunctionFoo(int,float);
void MyFunctionFoo(long,float);
?MyFunctionFoo@@YAXH(int,float);
\#void MyFunctionFoo^$@(long,float);

示例:

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;

void myswap(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}
void myswap(string& a, string& b) {
string tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 3, b = 4;
myswap(a, b);
cout << "a=" << a << ", b=" << b << endl;
string c = "apple", d = "banana";
myswap(c, d);
cout << "c=" << c << ", d=" << d << endl;
}

内联函数

C++将内联函数的代码组合到程序中,可以提高程序运行的速度。

语法:在函数声明和定义前加上关键字inline

通常的做法是将函数声明和定义写在一起

注意:

  • 内联函数节省时间,但消耗内存
  • 如果函数过大,编译器可能不将其作为内联函数
  • 内联函数不能递归

示例:

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;
inline void show(const short bh, const string message){ // 表白函数
cout << "亲爱的" << bh << "号:" << message << endl;
}
int main(){
//show(3, "我是一只傻傻鸟。");
{
int bh = 3;
string message = "我是一只傻傻鸟。";
cout << "亲爱的" << bh << "号:" << message << endl;
}
// show(8, "我有一只小小鸟。");
{
int bh = 8;
string message = "我有一只小小鸟。";
cout << "亲爱的" << bh << "号:" << message << endl;
}
// show(5, "我是一只小小鸟。");
{
int bh = 5;
string message = "我是一只小小鸟。";
cout << "亲爱的" << bh << "号:" << message << endl;
}
}

面向对象编程(OOP)

从结构体到类

对面向对象编程来说,一切都是对象,对象用类来描述

类把对象的数据和操作数据的方法作为一个整体考虑

定义类的语法:

1
2
3
4
5
6
7
8
class 类名
{
public:
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
......
成员n的数据类型 成员名n;
};

注意:

  • 类的成员可以是变量,也可以是函数
  • 类的成员变量也叫属性
  • 类的成员函数也叫方法/行为,类的成员函数可以定义在类的外面
  • 用类定义一个类的变量叫创建(或实例化)一个对象
  • 对象的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同
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
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。
struct st_girl {
string name; //名字
int age; // 年龄
int height; // 身高(cm)
double weight; // 体重(kg)
char sex = 'X'; // 性别:X-女,Y-男
int yz; // 颜值
string special; // 特长
string memo; // 备注
};
void setValue(st_girl &girl, string name, int age, int height,
double weight, int yz, string special, string memo) {
girl.name = name;
girl.age = age;
girl.height = height;
girl.weight = weight;
girl.yz = yz;
girl.special = special;
girl.memo = memo;
}
void show(const st_girl& girl) {
cout << "姓名:" << girl.name << ",年龄:" << girl.age << ",身高:" << girl.height
<< ",体重:" << girl.weight << ",颜值:" << girl.yz << ", 性别:" << girl.sex
<< ",特长:" << girl.special << ",备注:" << girl.memo << endl;

}
int main(){
st_girl girl;
setValue(girl, "西施", 26, 165, 45.5, 3, "唱歌", "无");
show(girl);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>         // 包含头文件
using namespace std; // 指定缺省的命名空间
struct st_girl{ // 超女基本信息结构体st_girl,存放了超女全部的数据项。
string name; // 姓名
int age; // 年龄
void setvalue(string name1, int age1){ // 设置成员变量的值。
name = name1; age = age1;
}
void show(){ // 显示超女的自我介绍
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
int main(){
st_girl girl; // 创建结构体变量。
girl.setvalue("西施", 26); // 设置成员变量的值。
girl.show(); // 显示超女的自我介绍。
}
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 CGirl {
public:
string name;
int age;
void setValue(string name1, int age1);
void show() {
cout << "姓名:" << name << ", 年龄:" << age << endl;
}
};
void CGirl::setValue(string name1, int age1) { // 设置成员变量的值
name = name1;
age = age1;
}
int main() {
CGirl girl;
girl.setValue("xiaomi", 26);
girl.show();
}

类的访问权限

类的成员有三种访问权限:publicprivateprotected,分别表示公有的、私有的和受保护的

在类的内部(类的成员函数中),无论成员被声明为public还是private,都可以访问

在类的外部(定义类的代码之外),只能访问public成员,不能访问 privateprotected成员

在一个类体的定义中,privatepublic可以出现多次

结构体的成员缺省为public,类的成员缺省为private

private的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public

简单使用类

编程思想和方法的改变,披着C++外衣的C程序员。

1)类的成员函数可以直接访问该类其它的成员函数(可以递归)。

2)类的成员函数可以重载,可以使用默认参数。

3)类指针的用法与结构体指针用法相同。

4)类的成员可以是任意数据类型(类中枚举)。

5)可以为类的成员指定缺省值(C++11标准)。

6)类可以创建对象数组,就像结构体数组一样。

7)对象可以作为实参传递给函数,一般传引用。

8)可以用new动态创建对象,用delete释放对象。

9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。数据隐藏是面向对象编程的思想之一。

10)对象一般不用memset()清空成员变量,可以写一个专用于清空成员变量的成员函数。

11)对类和对象用sizeof运算意义不大,一般不用。

12)用结构体描述纯粹的数据,用类描述对象。

13)在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了inline限定符,也是内联函数。

14)为了区分类的成员变量和成员函数的形参,把成员变量名加m_前缀或_后缀,如m_name或name_。

15)类的分文件编写。

构造函数和析构函数

构造函数:在创建对象时,自动的进行初始化工作。

析构函数:在销毁对象前,自动的完成清理工作。

1.构造函数

语法:类名(){......}

  • 访问权限必须是public
  • 函数名必须与类名相同
  • 没有返回值,不写void
  • 可以有参数,可以重载,可以有默认参数
  • 创建对象时只会自动调用一次,不能手工调用。
2.析构函数

语法:~类名(){......}

  • 访问权限必须是public
  • 函数名必须在类名前加~
  • 没有返回值,也不写void
  • 没有参数,不能重载
  • 销毁对象前只会自动调用一次,但是可以手工调用

注意:

1) 如果没有提供构造/析构函数,编译器将提供空实现的构造/析构函数。

2) 如果提供了构造/析构函数,编译器将不提供空实现的构造/析构函数。

3) 创建对象的时候,如果重载了构造函数,编译器根据实参匹配相应的构造函数。没有参数的构造函数也叫默认构造函数。

4) 创建对象的时候不要在对象名后面加空的圆括号,编译器误认为是声明函数。(如果没有构造函数、构造函数没有参数、构造函数的参数都有默认参数)

5) 在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象。

6) 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值(可能会导致问题,不推荐)。

CGirl girl =10;

7) 以下两行代码有本质的区别:

CGirl girl = CGirl("西施"20); // 显式创建对象。

CGirl girl; // 创建对象。

girl = CGirl("西施"20); // 创建匿名对象,然后给现有的对象赋值。

8) 用new/delete创建/销毁对象时,也会调用构造/析构函数。

9) 不建议在构造/析构函数中写太多的代码,可以调用成员函数。

10) 除了初始化,不建议让构造函数做太多工作(只能成功不会失败)。

11) C++11支持使用统一初始化列表。

CGirl girl = {"西施"20};

CGirl girl {"西施"20};

CGirl* girl = new CGirl{ "西施"20 };

12) 如果类的成员也是类,创建对象的时候,先构造成员类;销毁对象的时候,先析构自身,再析构成员类(视频中有误)。

示例:

评论