C++学习全教程(Day2)

一、数组

在程序中为了处理方便,常常需要把具有相同类型的数据对象按有序的形式排列起来,形成“一组”数据,这就是“数组”(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 = &pi;

	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函数指针作为形参

有了指向函数的指针,就给函数带来了更加丰富灵活的用法。比如,可以将函数指针作为形参,定义在另一个函数中。也就是说,可以定义一个函数,它以另一个函数类型作为形参。当然,函数本身不能作为形参,不过函数指针完美地填补了这个空缺。这一点上,函数跟数组非常类似。

3.4函数指针作为返回值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/759511.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis实战-添加商户缓存

为什么要使用缓存 言简意赅&#xff1a;速度快&#xff0c;好用缓存数据存储于代码中&#xff0c;而代码运行在内存中&#xff0c;内存的读写性能远高于磁盘&#xff0c;缓存可以大大降低用户访问并发量带来的服务器读写压力实际开发中&#xff0c;企业的数据量&#xff0c;少…

网络编程常见问题

1、TCP状态迁移图 2、TCP三次握手过程 2.1、握手流程 1、TCP服务器进程先创建传输控制块TCB&#xff0c;时刻准备接受客户进程的连接请求&#xff0c;此时服务器就进入了LISTEN&#xff08;监听&#xff09;状态&#xff1b; 2、TCP客户进程也是先创建传输控制块TCB&#xff…

RabbitMq教程【精细版一】

一、引言 模块之间的耦合度过高&#xff0c;导致一个模块宕机后&#xff0c;全部功能都不能用了&#xff0c;并且同步通讯的成本过高&#xff0c;用户体验差。 RabbitMQ引言 二、RabbitMQ介绍 MQ全称为Message Queue&#xff0c;消息队列是应用程序和应用程序之间的通信方法。…

如何利用AI生成可视化图表(统计图、流程图、思维导图……)免代码一键绘制图表

由于目前的AI生成图表工具存在以下几个方面的问题&#xff1a; 大多AI图表平台是纯英文&#xff0c;对国内用户来说不够友好&#xff1b;部分平台在生成图表前仍需选择图表类型、配置项&#xff0c;操作繁琐&#xff1b;他们仍需一份规整的数据表格&#xff0c;需要人为对数据…

碧海威L7云路由无线运营版 confirm.php/jumper.php 命令注入漏洞复现(XVE-2024-15716)

0x01 产品简介 碧海威L7网络设备是 北京智慧云巅科技有限公司下的产品,基于国产化ARM硬件平台,采用软硬一体协同设计方案,释放出产品最大效能,具有高性能,高扩展,产品性能强劲,具备万兆吞吐能力,支持上万用户同时在线等高性能。其采用简单清晰的可视化WEB管理界面,支持…

python序列

列表 与字符串的索引一样&#xff0c;列表索引从 0 开始&#xff0c;第二个索引是 1&#xff0c;依此类推。 通过索引列表可以进行截取、组合等操作 创建一个列表 list [red, green, blue, yellow, white, black]正向取值 print(list[1])反向取值 print(list[-2])更新列…

吉时利 Keithley2601B-PULSE 脉冲数字源表

Keithley2601B-PULSE吉时利脉冲SMU数字源表 无需手动脉冲调整即可实现高脉冲保真度 通过 2601B-PULSE 控制回路系统&#xff0c;高达 3μH 的负载变化无需手动调整&#xff0c;从而确保在任何电流水平&#xff08;最高 10 安培&#xff09;下输出 10 μs 至 500 μs 脉冲时&a…

【火猫】欧洲杯:西班牙老将去卡塔尔淘金,皇马赚麻了

欧洲杯正在如火如荼的进行中&#xff0c;球员的经纪人也在幕后紧罗密布的操作&#xff0c;已经有多位球员将会在新赛季更换门庭。目前正在西班牙国家队征战欧洲杯的老将何塞卢迎来了好消息&#xff0c;根据知名记者罗马诺爆料&#xff0c;何塞卢将会在下赛季加盟卡塔尔球队加拉…

本教程将指导如何通过 Vue 组件和后端 API 交互

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

HarmonyOS开发实战:加密类组件使用方法-API

加密类组件 模块介绍RSA提RSA供生成密钥加解密验签等系列方法(基于HarmonyOS API)AES提供AES生成密钥加解密等系列方法(基于HarmonyOS API)DES提供3DES生成密钥加解密等系列方法(基于HarmonyOS API)SM2提供SM2生成密钥加解密等系列方法(基于HarmonyOS API)SM3提供SM3生成摘要,…

HDFS详细介绍以及HDFS集群环境部署【hadoop组件HDFS笔记】(图片均为学习时截取的)

HDFS详细介绍 HDFS是什么 HDFS是Hadoop三大组件(HDFS、MapReduce、YARN)之一 全称是&#xff1a;Hadoop Distributed File System&#xff08;Hadoop分布式文件系统&#xff09;&#xff1b;是Hadoop技术栈内提供的分布式数据存储解决方案 可以在多台服务器上构建存储集群&…

42.HOOK引擎核心代码

上一个内容&#xff1a;41.HOOK引擎设计原理 以 40.设计HOOK引擎的好处 它的代码为基础进行修改 主要做的是读写寄存器 效果图 添加一个类 htdHook.h文件中的实现 #pragma once class htdHook { public:htdHook(); };htdHook.cpp文件中的实现&#xff1a; #include "…

论文阅读:Simple and Efficient Heterogeneous Graph Neural Network

Yang, Xiaocheng, Mingyu Yan, Shirui Pan, Xiaochun Ye and Dongrui Fan. “Simple and Efficient Heterogeneous Graph Neural Network.” AAAI Conference on Artificial Intelligence (2022). 论文地址&#xff1a;[PDF] Simple and Efficient Heterogeneous Graph Neural…

centos7 xtrabackup mysql 基本测试(5)mysql 建立 测试 数据库及内容

centos7 xtrabackup mysql 基本测试&#xff08;5&#xff09;mysql 建立 测试 数据库及内容 登录 mysql -u etc -p 1234aA~1创建数据库 名字是company show databases ; create database company;在 company里面 创建表employee use company; DROP TABLE IF EXISTS employ…

Webpack: 构建 NPM Library

概述 虽然 Webpack 多数情况下被用于构建 Web 应用&#xff0c;但与 Rollup、Snowpack 等工具类似&#xff0c;Webpack 同样具有完备的构建 NPM 库的能力。与一般场景相比&#xff0c;构建 NPM 库时需要注意&#xff1a; 正确导出模块内容&#xff1b;不要将第三方包打包进产…

C : 线性规划例题求解

Submit Page TestData Time Limit: 1 Sec Memory Limit: 128 Mb Submitted: 93 Solved: 49 Description 求解下述线性规划模型的最优值min &#xfffd;1&#xfffd;1&#xfffd;2&#xfffd;2&#xfffd;3&#xfffd;3&#xfffd;.&#xfffd;. &…

Pbootcms留言“提交成功”的提示语怎么修改

我们在用到pbootcms建站时候&#xff0c;其中有个留言功能&#xff0c;提交成功后会提示&#xff1a;提交成功&#xff08;如下图所示&#xff09;&#xff0c;那么我们要修改这个提示语要怎么操作呢&#xff1f; 如果需要修改的话&#xff0c;直接找到文件/apps/home/control…

完美世界|单机版合集(共22个版本)

前言 我是研究单机的老罗&#xff0c;今天给大家带来的是完美世界的单机版合集&#xff0c;一共22个版本。本人亲自测试了一个版本&#xff0c;运行视频如下&#xff1a; 完美世界|单机版合集 先看所有的版本的文件&#xff0c;文件比较大&#xff0c;准备好空间&#xff0c;差…

运行CDN

背景 CDN代码&#xff0c;调试运行 日常 git clone代码配置虚拟环境 puthon3.8,pip install r requirements.txt改项目数据集路径&#xff0c;在hico.py文件里面 # PATHS {# train: (root / images / train2015, root / annotations / trainval_hico.json),# val: …

捕获野生的登录页,暴改Vue3

1.实现效果 2.Vue组件 <script setup> import {onMounted} from "vue";onMounted(()>{// getAllData() }) </script><template><div class"login"><div class"form-cont"><div class"form-top"&…