一、数组
在程序中为了处理方便,常常需要把具有相同类型的数据对象按有序的形式排列起来,形成"一组"数据,这就是"数组"(array)
数组中的数据,在内存中是连续存放的,每个元素占据相同大小的空间,就像排好队一样。
1、数组的定义
数组的定义形式如下:
首先需要声明类型r数组中所有元素必须具有相同的数据类型;
数组名是一个标识符;后面跟着中括号,里面定义了数组中元素的个数,也就是数组的"长度";
元素个数也是类型的一部分,所以必须是确定的;
需要注意的是:
对数组做初始化,要使用花括号括起来的数值序列;
如果做了初始化,数组定义时的元素个数可以省略,编译器可以根据初始化列表自动推断出来;
初始值的个数,不能超过指定的元素个数;
初始值的个数,如果小于元素个数,那么会用列表中的值初始化靠前的元素;剩余元素用默认值填充,整型的默认值就是0;
如果没有做初始化,数组中元素的值都是未定义的;这一点和普通的局部变量一致;|
2、数组的访问
(1)访问数组元素
数组元素在内存中是连续存放的,它们排好了队之后就会有一个队伍中的编号,称为"索引",也叫"下标";通过下标就可以快速访问每个元素了,具体形式为:
这里也是用了中括号来表示元素下标位置,被称为"下标运算符"。比如 a[2]就表示数组a中下标为2的元素,可以取它的值输出,也可以对它赋值。
#include<iostream>
using namespace std;
int main()
{
int a[] = {1,2,3,4,5,6};
cout << "a[1] = " << a[1] << endl;
a[1] = 666;
cout << "a[1] = " << a[1] << endl;
cin.get();
}
运行结果:
需要注意的是:
数组的下标从o开始;
因此 a[2]访问的并不是数组 a的第2个元素,而是第三个元素;一个长度为10的数组,下标范围是o~9,而不是1~10;
合理的下标,不能小于o,也不能大于(数组长度-1);否则就会出现数组下标越界;
(2)数组的大小
所有的变量,都会在内存中占据一定大小的空间;而数据类型就决定了它具体的大小。而对于数组这样的"复合类型",由于每个元素类型相同,因此占据空间大小的计算遵循下面的简单公式:
#include<iostream>
using namespace std;
int main()
{
int a[] = {1,2,3,4,5,6};
cout << "a[1] = " << a[1] << endl;
a[1] = 666;
cout << "a[1] = " << a[1] << endl;
//获取数组大小的长度
cout << "数组a所占空间大小为:" << sizeof(a) << endl;
cout << "数组a中每个元素的大小:" << sizeof(a[0]) << endl;
int size;
size = sizeof(a) / sizeof(a[0]);
cout << "数组a的长度为:" << size << endl;
cin.get();
}
运行结果:
3、多维数组
C++中本质上没有"多维数组"这种东西,所谓的"多维数组",其实就是"数组的数组"。
二维数组int arr[3][4]表示: arr是一个有三个元素的数组,其中的每个元素都是一个int 数组,包含4个元素;
三维数组int arr2[2][5][10]表示: arr2是一个长度为2的数组,其中每个元素都是一个二维数组;这个二维数组有5个元素,每个元素都是一个长度为10的int 数组;
一般最常见的就是二维数组。它有两个"维度",第一个维度表示数组本身的长度,第二个表示每个元素的长度,一般分别把它们叫做"行"和"列"。
(1)多维数组的初始化
和普通的"一维"数组一样,多维数组初始化时,也可以用花括号括起来的一组数。使用嵌套的花括号可以让不同的维度更清晰:
需要注意:
内嵌的花括号不是必需的,因为数组中的元素在内存中连续存放,可以用一个花括号将所有数据括在一起;
初始值的个数,可以小于数组定义的长度,其它元素初始化为0值;这一点对整个二维数组和每一行的一维数组都适用;
如果省略嵌套的花括号,当初始值个数小于总元素个数时,会按照顺序依次填充(填满第一行,才填第二行);其它元素初始化为О值;
多维数组的维度,可以省略第一个,由编译器自动推断;即二维数组可以省略行数,但不能省略列数。
#include<iostream>
using namespace std;
int main()
{
//初始化
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int b[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int c[3][4] = { 1,2,3,4 };
int d[][4] = { 1,2,3,4,5,6 };
//访问
cout << "b[1][1] = " << b[1][1] << endl;
b[1][1] = 666;
cout << "b[1][1] = " << b[1][1] << endl;
//遍历
//计算二维数组中的行数和列数
cout << "二维数组b的总大小:" << sizeof(b) << endl; //大小说的是字节
cout << "二维数组每一行的大小:" << sizeof(b[0]) << endl;
cout << "二维数组中每一个元素的大小:" << sizeof(b[0][0]) << endl;
int rowCnt = sizeof(b) / sizeof(b[0]); //行数
int colCnt = sizeof(b[0]) / sizeof(b[0][0]); //列数
cout << "rowCnt = " << rowCnt << endl;
cout << "colCnt = " << colCnt << endl;
for (int i = 0; i < rowCnt; i++)
{
for (int j = 0; j < colCnt; j++)
{
cout << b[i][j] << "\t";
}
cout << endl;
}
cin.get();
}
运行结果:
4、数组的排序
数组排序指的是给定一个数组,要求把其中的元素按照从小到大(或从大到小)顺序排列。
这是一个非常经典的需求,有各种不同的算法可以实现。我们这里介绍两种最基本、最简单的排序算法。
(1)选择排序
选择排序是一种简单直观的排序算法。
它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后追加到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
#include<iostream>
using namespace std;
int main()
{
int a[] = {2,5,8,4,9,1,6,3,7,0};
//计算数组a的大小
int size = sizeof(a) / sizeof(a[0]);
//选择排序
for (int i = 0; i < size; i++)
{
for (int j = i + 1; j < size; j++)
{
if (a[j] < a[i])
{
int temp = a[j];
a[j] = a[i];
a[i] = temp;
}
}
}
for (int num : a)
{
cout << num << "\t";
}
cout << endl;
cin.get();
}
运行结果:
(2)冒泡排序
冒泡排序也是一种简单的排序算法。
它的基本原理是:重复地扫描要排序的数列,一次比较两个元素,如果它们的大小顺序错误,就把它们交换过来。这样,一次扫描结束,我们可以确保最大(小)的值被移动到序列末尾。这个算法的名字由来,就是因为越小的元素会经由交换,慢慢"浮"到数列的顶端。
#include<iostream>
using namespace std;
int main()
{
int a[] = {2,5,8,4,9,1,6,3,7,0};
//计算数组a的大小
int size = sizeof(a) / sizeof(a[0]);
//冒泡排序
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
if (a[j] > a[j + 1])
{
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
}
for (int num : a)
{
cout << num << "\t";
}
cout << endl;
cin.get();
}
运行结果:
二、模板类vector
数组尽管很灵活,但使用起来还是很多不方便。为此,C++语言定义了扩展的"抽象数据类型"(Abstract Data Type,ADT),放在"标准库"中。
对数组功能进行扩展的一个标准库类型,就是"容器"vector。顾名思义,vector"容纳"着一堆数据对象,其实就是一组类型相同的数据对象的集合。
1、头文件和命名空间
vector是标准库的一部分。要想使用vector,必须在程序中包含<vector>头文件,并使用std命名空间。
在vector头文件中,对vector这种类型做了定义;使用#include引入它之后,并指定命名空间std之后,我们就可以在代码中直接使用vector了。
2、 vector的基本用法
vector其实是C++中的一个"类模板",是用来创建类的"模子"。所以在使用时还必须提供具体的类型信息,也就是说,这个容器中到底要容纳什么类型的数据对象;具体的形式是在vector后面跟一个尖括号<>,里面填入具体类型信息。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
//默认初始化
vector<int> v1; //定义了整型的容器,容器名:v1
//列表初始化
vector<char> v2 = { 'a','b','c' };
vector<char> v3{ 'a','b','c' };
//直接初始化
vector<short> v4(5); //定义了大小为5的短整型容器
vector<double> v5(5,100);//定义了大小为5的double容器,且每个值都为100
//访问元素
cout << v5[2] << endl;
v5[2] = 666;
cout << v5[2] << endl;
//遍历所有元素
for (int i = 0; i < v5.size(); i++)
{
cout << v5[i] << "\t";
}
cout << endl;
//添加元素
v5.push_back(69);
for (int num : v5)
{
cout << num << "\t";
}
cout << endl;
//向容器中添加倒序的元素
for (int i = 10; i > 0; i--)
{
v1.push_back(i);
}
//遍历容器v1
for (int num : v1)
{
cout << num << "\t";
}
cout << endl;
cin.get();
}
运行结果:
2、vector和数组的区别
数组是更加底层的数据类型;长度固定,功能较少,安全性没有保证;但性能更好,运行更高效;
vector是模板类,是数组的上层抽象;长度不定,功能强大;缺点是运行效率较低;
除了vector之外,C++ 11还新增了一个array模板类,它跟数组更加类似,长度是固定的,但更加方便、更加安全。所以在实际应用中,一般推荐对于固定长度的数组使用array,不固定长度的数组使用vector。
三、字符串
字符串我们并不陌生。之前已经介绍过,一串字符连在一起就是一个"字符串",比如用双引号引起来的"Hello World !"就是一个字符串字面值。
字符串其实就是所谓的"纯文本",就是各种文字、数字、符号在一起表达的一串信息;所以字符串就是C++中用来表达和处理文本信息的数据类型。
1、标准库类型string
C++的标准库中,提供了一种用来表示字符串的数据类型string,这种类型能够表示长度可变的字符序列。和 vector类似,Istring 类型也定义在命名空间std中,使用它必须包含string头文件。
(1)定义和初始化 string
我们已经接触过C++中几种不同的初始化方式, string 也是一个标准库类型,它的初始化与vector非常相似。
#include<iostream>
#include<string>
using namespace std;
int main()
{
//初始化
string s1;
//拷贝初始化
string s2 = s1;
string s3 = "hello world";
//直接初始化
string s4("hello world");
string s5(8, 'h');
cout << s5 << endl;
//访问字符
cout << "s4[1] = " << s4[1] << endl;
s4[0] = 'H';
cout << s4 << endl;
cin.get();
}
运行结果:
字符串内字符的访问,跟vector内元素的访问类似,需要注意:
string内字符的索引,也是从o开始;
string 同样有一个成员函数size,可以获取字符串的长度;
索引最大值为字符串长度- 1),不能越界访问;如果直接越界访问并赋值,有可能导致非常严重的后果,出现安全问题;
如果希望遍历字符串的元素,也可以使用普通for 循环和范围for 循环,依次获取每个字符
#include<iostream>
#include<string>
using namespace std;
int main()
{
//初始化
string s1;
//拷贝初始化
string s2 = s1;
string s3 = "hello world";
//直接初始化
string s4("hello world");
string s5(8, 'h');
cout << s5 << endl;
//访问字符
cout << "s4[1] = " << s4[1] << endl;
s4[0] = 'H';
cout << s4 << endl;
//遍历
for (int i = 0; i < s4.size(); i++)
{
s4[i] = toupper(s4[i]); //toupper:小写转换大写
}
cout << s4 << endl;
cin.get();
}
运行结果:
(2)字符串相加
string本身的长度是不定的,可以通过"相加"的方式扩展一个字符串。
需要注意:
字符串相加使用加号"E"来表示,这是算术运算符"+"的运算符重载,含义是"字符串拼接";
两个string对象,可以直接进行字符串相加;结果是将两个字符串拼接在一起,得到一个新的string对象返回;
一个string对象和一个字符串字面值常量,可以进行字符串相加,同样是得到一个拼接后的string对象返回;
两个字符串字面值常量,不能相加;
多个string 对象和多个字符串字面值常量,可以连续相加;前提是按照左结合律,每次相加必须保证至少有一个string对象;
(3)比较字符串
字符串比较的规则为:
如果两个字符串长度相同,每个位置包含的字符也都相同,那么两者"相等";否则"不相等";
如果两个字符串在某一位置上开始不同,那么就比较这两个字符的 ASCIl码,比较结果就代表两个字符串的大小关系
如果两个字符串在某一位置上开始不同,那么就比较这两个字符的 ASCIl码,比较结果就代表两个字符串的大小关系
2、字符数组
通过对string的介绍可以发现,字符串就是一串字符的集合,本质上其实就是一个"字符的数组"。
在C语言中,确实是用char[]类型来表示字符串的;不过为了区分纯粹的"字符数组"和"字符串",C语言规定:字符串必须以空字符结束。空字符的ASClI码为o,专门用来标记字符串的结尾,在程序中写作\0'。
#include<iostream>
using namespace std;
int main()
{
char str1[5] = { 'g','y','s','z','s' };//str1不是一个字符串
char str2[6] = { 'g','y','s','z','s','\0'};//str2是一个字符串
cout << "str1 = " << str1 << endl;
cout << "str2 = " << str2 << endl;
cin.get();
}
运行结果:
3、读取输入的字符串
(1)使用输入操作符读取单词
标准库中提供了iostream,可以使用内置的 cin对象,调用重载的输入操作符>>来读取键盘输入。
这种方式的特点是:忽略开始的空白符,遇到下一个空白符(空格、回车、制表等)就会停止。所以如果我们输入""hello world",那么读取给str的只有"hello"这相当于读取了一个"单词"。
剩下的内容"world"其实也没有丢,而是保存在了输入流的"输入队列"里。如果我们想读取更多的输入信息,就需要使用更多的string对象来获取:
(2〉使用getline读取一行
如果希望直接读取一整行输入信息,可以使用getline函数来替代输入操作符。
getline函数有两个参数:一个是输入流对象cin,另一个是保存字符串的string 对象;它会一直读取输入流中的内容,直到遇到换行符为止,然后把所有内容保存到string 对象中。所以现在可以完整读取一整行信息了。
#include<iostream>
#include<string>
using namespace std;
int main()
{
//使用getline读取一行信息
string str1;
getline(cin, str1);
cin.get();
cout << "str1 = " << str1 << endl;
cin.get();
}
运行结果:
(3)使用get读取字符
还有一种方法,是调用cin 的 get函数读取一个字符。
有两种方式:
调用cin.get()函数,不传参数,得到一个字符赋给char类型变量;
将char类型变量作为参数传入,将捕获的字符赋值给它,返回的是istream对象
(4)读写文件
C++的lo库中提供了专门用于文件输入的 ifstream类和用于文件输出的ofstream类,要使用它们需要引入头文件 fstream。ifstream 用于读取文件内容,跟istream 的用法类似;也可以通过输入操作符>>来读"单词"(空格分隔),通过getline 函数来读取一行,通过get函数来读取一个字符:
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
ifstream input("1.txt"); //通过input对象来从文件中读取数据
ofstream output("2.txt");
//1.按照单词逐个读取
/*string word;
while (input >> word)
{
cout << word << endl;
}*/
//2.逐行读取
string line;
while (getline(input, line))
{
cout << line << endl;
//output << line << endl;
}
cin.get();
}
运行结果:
四、结构体
C/C++中提供了另一种更加灵活的数据结构------结构体。结构体是用户自定义的复合数据结构,里面可以包含多个不同类型的数据对象。
1、结构体的定义和声明
声明一个结构体需要使用struct关键字,具体形式如下:
#include<iostream>
using namespace std;
//定义1个结构体
struct student
{
string name;
int age;
double score;
};
int main()
{
//创建对象并初始化
student s1 = { "ljl",18,99 };
cin.get();
}
2、访问结构体中数据
访问结构体变量中的数据成员,可以使用成员运算符点号.),后面跟上数据成员的名称。例如stu.name就可以访问stu对象的name成员。
#include<iostream>
using namespace std;
//定义1个结构体
struct student
{
string name;
int age;
double score;
};
//输出一个数据对象的完整信息
void pintinfo(student s)
{
//访问数据
cout << "学生姓名:" << s.name << "\t年龄:" << s.age << "\t成绩:" << s.score << endl;
}
int main()
{
//创建对象并初始化
student s1 = { "ljl",18,99 };
student s2;
s2.age = 20;
s2.name = "xiaoming";
s2.score = 80;
pintinfo(s1);
pintinfo(s2);
cin.get();
}
运行结果:
3、结构体数组
可以把结构体和数组结合起来,创建结构体的数组。顾名思义,结构体数组就是元素为结构体的数组,它的定义和访问跟普通的数组完全一样。
#include<iostream>
using namespace std;
//定义1个结构体
struct student
{
string name;
int age;
double score;
};
//输出一个数据对象的完整信息
void pintinfo(student s)
{
//访问数据
cout << "学生姓名:" << s.name << "\t年龄:" << s.age << "\t成绩:" << s.score << endl;
}
int main()
{
//创建对象并初始化
student s1 = { "ljl",18,99 };
student s2;
s2.age = 20;
s2.name = "xiaoming";
s2.score = 80;
pintinfo(s1);
pintinfo(s2);
//结构体数组
student s3[3] = {
{"小王",18,88},
{"小张",19,66},
{"小钱",20,99}
};
pintinfo(s3[0]);
cin.get();
}
运行结果:
五、枚举
枚举类型的定义和结构体非常像,需要使用enum关键字。
与结构体不同的是,枚举类型内只有有限个名字,它们都各自代表一个常量,被称为"枚举量"。
需要注意的是:
默认情况下,会将整数值赋给枚举量;
枚举量默认从0开始,每个枚举量依次加1;所以上面week枚举类型中,一周七天枚举量分别对应着0~6的常量值;
可以通过对枚举量赋值,显式地设置每个枚举量的值;
#include<iostream>
using namespace std;
//定义1个枚举类型
enum Week {
Mon, Tue, Wed, Thu, Fri, Sta, Sun
};
int main()
{
Week w1 = Mon, w2 = Tue;
cout << "w1 = " << w1 << endl;
cout << "w2 = " << w2 << endl;
cin.get();
}
运行结果:
六、指针 ---存放是地址
指针顾名思义,是"指向"另外一种数据类型的复合类型。指针是C/C++中一种特殊的数据类型,它所保存的信息,其实是另外一个数据对象在内存中的"地址"。通过指针可以访问到指向的那个数据对象,所以这是一种间接访问对象的方法。
1、指针的定义
这里的类型就是指针所指向的数据类型,后面加上星号"*",然后跟指针变量的名称。指针在定义的时候可以不做初始化。相比一般的变量声明,看起来指针只是多了一个星号"*"而已。例如:
#include<iostream>
using namespace std;
int main()
{
int* p;
long* p1;
long long* p2;
cout << "p在内存中的长度为:" << sizeof(p) << endl;
cout << "p1在内存中的长度为:" << sizeof(p1) << endl;
cout << "p2在内存中的长度为:" << sizeof(p2) << endl;
cin.get();
}
运行结果:
指针在64位系统下占据8个字节,在32位系统下占4个字节。
2、指针的用法
(1)获取对象地址给指针赋值
指针保存的是数据对象的内存地址,所以可以用地址给指针赋值;获取对象地址的方式是使用"取地址操作符"((&)。
#include<iostream>
using namespace std;
int main()
{
int* p;
long* p1;
long long* p2;
cout << "p在内存中的长度为:" << sizeof(p) << endl;
cout << "p1在内存中的长度为:" << sizeof(p1) << endl;
cout << "p2在内存中的长度为:" << sizeof(p2) << endl;
//指针的使用
int a = 10;
long b = 20;
p = &a;
p1 = &b;
cout << "a的地址为:" << &a << endl;
cout << "b的地址为:" << &b << endl;
cout << "p:" << p << endl;
cout << "p1:" << p1 << endl;
cin.get();
}
运行结果:
(2)通过指针访问对象
指针指向数据对象后,可以通过指针来访问对象。访问方式是使用"解引用操作符"(*):
在这里由于p指向了a,所以*p可以等同于a。
#include<iostream>
using namespace std;
int main()
{
int* p;
long* p1;
long long* p2;
cout << "p在内存中的长度为:" << sizeof(p) << endl;
cout << "p1在内存中的长度为:" << sizeof(p1) << endl;
cout << "p2在内存中的长度为:" << sizeof(p2) << endl;
//指针的使用
int a = 10;
long b = 20;
p = &a;
p1 = &b;
cout << "a的地址为:" << &a << endl;
cout << "b的地址为:" << &b << endl;
cout << "p:" << p << endl;
cout << "p1:" << p1 << endl;
*p = 666;
cout << "a = " << a << endl;
cin.get();
}
运行结果:
3、无效指针、空指针和 void*指针
(1)无效指针
定义一个指针之后,如果不进行初始化,那么它的内容是不确定的(比如Oxcccc)。如果这时把它的内容当成一个地址去访问,就可能访问的是不存在的对象;更可怕的是,如果访问到的是系统核心内存区域,修改其中内容会导致系统崩溃。这样的指针就是"无效指针",也被叫做"野指针"。
指针非常灵活非常强大,但野指针非常危险。所以建议使用指针的时候,一定要先初始化,让它指向真实的对象。
(2)空指针
如果先定义了一个指针,但确实还不知道它要指向哪个对象,这时可以把它初始化为"空指针"。空指针不指向任何对象。
(3)void* 指针
一般来说,指针的类型必须和指向的对象类型匹配,否则就会报错。不过有一种指针比较特殊,可以用来存放任意对象的地址,这种指针的类型是void*。
4、指向指针的指针
指针本身也是一个数据对象,也有自己的内存地址。所以可以让一个指针保存另一个指针的地址,这就是"指向指针的指针",有时也叫"二级指针";形式上可以用连续两个的星号**来表示。类似地,如果是三级指针就是***,表示"指向二级指针的指针"。
#include<iostream>
using namespace std;
int main()
{
int* p;
long* p1;
long long* p2;
cout << "p在内存中的长度为:" << sizeof(p) << endl;
cout << "p1在内存中的长度为:" << sizeof(p1) << endl;
cout << "p2在内存中的长度为:" << sizeof(p2) << endl;
//指针的使用
int a = 10;
long b = 20;
p = &a;
p1 = &b;
cout << "a的地址为:" << &a << endl;
cout << "b的地址为:" << &b << endl;
cout << "p:" << p << endl;
cout << "p1:" << p1 << endl;
*p = 666;
cout << "a = " << a << endl;
cout << "--------------------------------" << endl;
//二级指针
int i = 100;
int* pi = &i;
int** ppi = π
cout << "i = " << i << endl;
cout << "pi = " << pi << endl;
cout << "ppi = " << ppi << endl;
cout << "*pi = " << *pi << endl;
cout << "*ppi = " << *ppi << endl;
cout << "**ppi = " << **ppi << endl;
cin.get();
}
运行结果:
5、指针和const
指针可以和 const 修饰符结合,这可以有两种形式:一种是指针指向的是一个常量;另一种是指针本身是一个常量。
(1)指向常量的指针
指针指向的是一个常量,所以只能访问数据,不能通过指针对数据进行修改。不过指针本身是变量,可以指向另外的数据对象。这时应该把const加在类型前。
这里发现,pc是一个指向常量的指针,但其实把一个变量i的地址赋给它也是可以的;编译器只是不允许通过指针pc去间接更改数据对象。
(2)指针常量(const 指针)
指针本身是一个数据对象,所以也可以区分变量和常量。如果指针本身是一个常量,就意味它保存的地址不能更改,也就是它永远指向同一个对象;而数据对象的内容是可以通过指针改变的。这种指针一般叫做"指针常量"。
指针常量在定义的时候,需要在星号*后、标识符前加上 const。
6、指针和数组
(1)数组名
用到数组名时,编译器一般都会把它转换成指针,这个指针就指向数组的第一个元素。所以我们也可以用数组名来给指针赋值。
运行结果:
(2)指针运算
如果对指针pia 做加1操作,我们会发现它保存的地址直接加了4,这其实是指向了下一个int类型数据对象:
所谓的"指针运算",就是直接对一个指针加/减一个整数值,得到的结果仍然是指针。新指针指向的数据元素,跟原指针指向的相比移动了对应个数据单位。
(3)指针数组和数组指针
指针和数组这两种类型可以结合在一起,这就是"指针数组"和"数组指针"。
指针数组:一个数组,它的所有元素都是相同类型的指针;
数组指针:一个指针,指向一个数组的指针;
运行结果:
七、引用
1、引用的用法
在做声明时,我们可以在变量名前加上"&"符号,表示它是另一个变量的引用。引用必须被初始化。
#include<iostream>
using namespace std;
int main()
{
int a = 888, b = 666;
int& ref = a;
cout << "ref = " << ref << endl;
cout << "a的地址为: " << &a << endl;
cout << "ref的地址为: " << &ref << endl;
cin.get();
}
运行结果:
引用本质上就是一个"别名",它本身不是数据对象,所以本身不会存储数据,而是和初始值"绑定"(bind)在一起,绑定之后就不能再绑定别的对象了。
2、对常量的引用
可以把引用绑定到一个常量上,这就是"对常量的引用"。很显然,对常量的引用是常量的别名,绑定的对象不能修改,所以也不能做赋值操作:
对常量的引用有时也会直接简称"常量引用"。因为引用只是别名,本身不是数据对象;所以这只能代表"对一个常量的引用",而不会像"常量指针"那样引起混淆。
常量引用和普通变量的引用不同,它的初始化要求宽松很多,只要是可以转换成它指定类型的所有表达式,都可以用来做初始化。
3、指针和引用
(1)引用和指针常量
事实上,引用的行为,非常类似于"指针常量",也就是只能指向唯一的对象、不能更改的指针。
#include<iostream>
using namespace std;
int main()
{
int a = 66;
//引用和指针常量
int& ref = a;
int* const p = &a;
ref = 666;
cout << "a = " << a << endl;
*p = 888;
cout << "a = " << a << endl;
cout << "a的地址为:" << &a << endl;
cout << "ref的地址为:" << &ref << endl;
cout << "p的地址为:" << &p << endl;
cout << "p的值为: " << p << endl;
//绑定指针的引用
int* ptr = &a;
int*& pref = ptr; //pref是ptr指针的别名
*ptr = 100;
cout << "a = " << a << endl;
cin.get();
}
运行结果:
八、函数
8.1、函数的定义
一个完整的函数定义主要包括以下部分:
返回类型:调用函数之后,返回结果的数据类型;
函数名:用来命名代码块的标识符,在当前作用域内唯一;
参数列表:参数表示函数调用时需要传入的数据,一般叫做"形参";放在函数名后的小括号里,可以有o个或多个,用逗号隔开;
函数体:函数要执行的语句块,用花括号括起来。
#include<iostream>
using namespace std;
//定义一个平方函数
int square(int x)
{
int y = x * x;
return y;
}
int main()
{
cout << "请输入一个整数:";
int num;
cin >> num;
cin.get();
cout << "num的平方为:" << square(num) << endl;
cin.get();
}
运行结果:
8.1.2、局部变量的生命周期
对于花括号内定义的变量,具有"块作用域",在花括号外就不可见了。函数体都是语句块,而主函数main本身也是一个函数;所以在main 中定义的所有变量、所有函数形参和在函数体内部定义的变量,都具有块作用域,统称为"局部变量"。局部变量仅在函数作用域内部可见。
在C++中,作用域指的是变量名字的可见范围;变量不可见,并不代表变量所指代的数据对象就销毁了。这是两个不同的概念:
作用域:针对名字而言,是程序文本中的一部分,名字在这部分可见;
生命周期:针对数据对象而言,是程序在执行过程中,对象从创建到销毁的时间段
基于作用域,变量可以分为"局部变量"和"全局变量"。对于全局变量而言,名字全局可见,对象也只有在程序结束时才销毁。
而对于局部变量代表的数据对象,基于生命周期,又可以分为"自动对象"和"静态对象"。
(1)自动对象
平常代码中定义的普通局部变量,生命周期为:在程序执行到变量定义语句时创建,在程序运行到当前块末尾时销毁。这样的对象称为"自动对象"。
形参也是一种自动对象。形参定义在函数体作用域内,一旦函数终止,形参也就被销毁了。
对于自动对象来说,它的生命周期和作用域是一致的。
(2)静态对象
如果希望延长一个局部变量的生命周期,让它在作用域外依然保留,可以在定义局部变量时加上static关键字;这样的对象叫做"局部静态对象"。
局部静态对象只有局部的作用域,在块外依然是不可见的;但是它的生命周期贯穿整个程序运行过程,只有在程序结束时才被销毁,这一点与全局变量类似。
8.1.3、分离式编译和头文件
(1)分离式编译
当程序越来越复杂,我们就会希望代码分散到不同的文件中来做管理。C++支持分离式编译,这就可以把函数单独放在一个文件,独立编译之后链接运行。
比如可以把复制字符串的函数单独保存成一个文件 copy_string.cpp:
(2)编写头文件
对于一个项目而言,有些定义可能是所有文件共用的,比如一些常量、结构体/类,以及功能性的函数。于是每次需要引入时,都得做一堆声明------这显然太麻烦了。
一个好方法是,把它们定义在同一个文件中,需要时用一句#include 统一引入就可以了,就像使用库一样。这样的文件以.h作为后缀,被称为"头文件"。
8.2、参数传递
参数传递和变量的初始化类似,根据形参的类型可以分为两种方式:传值(value)和传引用(reference)。
8.2.1、传引用参数
(1)传引用方便函数调用
C++新增了引用的概念,可以替换必须使用指针的场景。采用引用作为函数形参,可以使函数调用更加方便。这种传参方式叫做"传引用参数"。之前的例子就可以改写成:
#include<iostream>
using namespace std;
//使用引用作为形参
void increase(int& x)
{
++x;
}
int main()
{
int num = 0;
increase(num);
cout << "num = " << num << endl;
cin.get();
}
运行结果:
(2)传引用避免拷贝
使用引用还有一个非常重要的场景,就是不希望进行值拷贝的时候。实际应用中,很多时候函数要操作的对象可能非常庞大,如果做值拷贝会使得效率大大降低;这时使用引用就是一个好方法。
比如,想要定义一个函数比较两个字符串的长度,需要将两个字符串作为参数传入。因为字符串有可能非常长,直接做值拷贝并不是一个好选择,最好的方式就是传递引用:
8.2.2、数组形参
数组是不允许做直接拷贝的,所以如果想要把数组作为函数的形参,使用值传递的方式是不可行的。与此同时,数组名可以解析成一个指针,所以可以用传递指针的方式来处理数组。|
比如一个简单的函数,需要遍历int类型数组所有元素并输出,就可以这样声明:
#include<iostream>
using namespace std;
//遍历数组
void printArray(const int* arr, int size)
{
for (int i = 0; i < size; i++)
{
cout << arr[i] << "\t";
}
cout << endl;
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
printArray(arr, 5);
cin.get();
}
运行结果:
8.2.3、数组引用作为形参
#include<iostream>
using namespace std;
//遍历数组
void printArray(const int* arr, int size)
{
for (int i = 0; i < size; i++)
{
cout << arr[i] << "\t";
}
cout << endl;
}
void printArray(const int(& arr)[5])
{
for (int num : arr)
{
cout << num << "\t";
}
cout << endl;
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
printArray(arr, 5);
printArray(arr);
cin.get();
}
运行结果:
8.3.1、无返回值
#include<iostream>
using namespace std;
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 66, b = 88;
swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cin.get();
}
运行结果:
8.3.2、有返回值
#include<iostream>
using namespace std;
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
//有返回值的函数,返回较长的字符串
string longstr(const string& str1, const string& str2)
{
return str1.size() >= str2.size() ? str1 : str2;
}
int main()
{
int a = 66, b = 88;
swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
string str1 = "hello";
string str2 = "hello world";
cout << longstr(str1, str2) << endl;
cin.get();
}
运行结果:
8.3.3、返回数组指针
这里对于函数fun的声明,我们可以进行层层解析:
fun(int x):函数名为fun,形参为int类型的x;
(*fun(int 炳 ):函数返回的结果,可以执行解引用操作,说明是一个指针;
( *fun(int x))[5]:函数返回结果解引用之后是一个长度为5的数组,说明返回类型是数组指针;
int ( * fun(int x) )[5]:数组中元素类型为int
九、递归
如果一个函数调用了自身,这样的函数就叫做"递归函数"(recursivefunction)。
1、递归的实现
递归是调用自身,如果不加限制,这个过程是不会结束的;函数永远调用自己下去,最终会导致程序栈空间耗尽。所以在递归函数中,一定会有某种"基准情况",这个时候不会调用自身,而是直接返回结果。基准情况的处理保证了递归能够结束。|
递归是不断地自我重复,这一点和循环有相似之处。事实上,递归和循环往往可以实现同样的功能。
#include<iostream>
using namespace std;
//用递归实现阶乘
int factorial(int n)
{
if (n == 1)
return 1;
return factorial(n - 1) * n;
}
int main()
{
cout << "5! = " << factorial(5) << endl;
cin.get();
}
运行结果:
这里我们的基准情况是n ==1,也就是当n不断减小,直到1时就结束递归直接返回。5的阶乘具体计算流程如下:
因为递归至少需要额外的栈空间开销,所以递归的效率往往会比循环低一些。不过在很多数学问题上,递归可以让代码非常简洁。
2、经典递归-------斐波那契数列
#include<iostream>
using namespace std;
//用递归实现阶乘
int factorial(int n)
{
if (n == 1)
return 1;
return factorial(n - 1) * n;
}
//斐波那契数列
int fib(int n)
{
if (n == 1 || n == 2)
return 1;
return fib(n - 1) + fib(n - 2);
}
int main()
{
cout << "5! = " << factorial(5) << endl;
cout << "fib(5) = " << fib(5) << endl;
cin.get();
}
运行结果:
十、函数高阶
1、内联函数
内联函数是C++为了提高运行速度做的一项优化。
函数让代码更加模块化,可重用性、可读性大大提高;不过函数也有一个缺点:函数调用需要执行一系列额外操作,会降低程序运行效率。
为了解决这个问题,C++引入了"内联函数"的概念。使用内联函数时,编译器不再去做常规的函数调用,而是把它在调用点上"内联"展开,也就是直接用函数代码替换了函数调用。
1.1内联函数的定义
定义内联函数,只需要在函数声明或者函数定义前加上 inline关键字。
1.2内联函数和宏
内联函数是C++新增的特性。在C语言中,类似功能是通过预处理语句和#define定义"宏"来实现的。
然而c中的宏本身并不是函数,无法进行值传递;它的本质是文本替换,我们一般只用宏来定义常量。用宏实现函数的功能会比较麻烦,而且可读性较差。所以在C++中,一般都会用内联函数来取代C中的宏。
2、函数重载
在C++中,同一作用域下,同一个函数名是可以定义多次的,前提是形参列表不同。这种名字相同但形参列表不同的函数,叫做"重载函数"。这是C++相对c语言的重大改进,也是面向对象的基础。
2.1定义重装函数
重载的函数,应该在形参的数量或者类型上有所不同;
形参的名称在类型中可以省略,所以只有形参名不同的函数是一样的;
调用函数时,编译器会根据传递的实参个数和类型,自动推断使用哪个函数;
主函数不能重载
2.2函数匹配
确定到底调用哪个函数的过程,叫做"函数匹配"。
#include<iostream>
using namespace std;
void f() { cout << "1" << endl; }
void f(int x) { cout << "2" << endl; }
void f(int x, int y) { cout << "3" << endl; }
void f(double x, double y = 1.5) { cout << "4" << endl; }
int main()
{
f(3.14);
cin.get();
}
运行结果:
2.3函数重装和作用域
重载是否生效,跟作用域是有关系的。如果在内层、外层作用域分别声明了同名的函数,那么内层作用域中的函数会覆盖外层的同名实体,让它隐藏起来。
不同的作用域中,是无法重载函数名的。
3、函数指针
一类特殊的指针,指向的不是数据对象而是函数,这就是"函数指针"
3.1声明函数指针
函数指针本质还是指针,它的类型和所指向的对象类型有关。现在指向的是函数,函数的类型是由它的返回类型和形参类型共同决定的,跟函数名、形参名都没有关系。
3.2使用函数指针
当一个函数名后面跟调用操作符(小括号),表示函数调用;而单独使用函数名作为一个值时,函数会自动转换成指针。这一点跟数组名类似。
所以我们可以直接使用函数名给函数指针赋值:
也可以加上取地址符&,这和不加&是等价的。
3.3函数指针作为形参
有了指向函数的指针,就给函数带来了更加丰富灵活的用法。比如,可以将函数指针作为形参,定义在另一个函数中。也就是说,可以定义一个函数,它以另一个函数类型作为形参。当然,函数本身不能作为形参,不过函数指针完美地填补了这个空缺。这一点上,函数跟数组非常类似。