【C++基础】变量和数据类型

变量和数据类型

引言1:为什么需要变量?

一段程序的核心有两个方面:一个是要处理的信息,另一个就是处理的计算流程。计算机所处理的信息一般叫做数据(data)。
对计算机来说,需要明确地知道把数据存放在哪里、以及需要多大的存储空间。在机器语言和汇编语言中,我们可能需要充分了解计算机底层的存储空间,这非常麻烦;而在C++程序中,我们可以通过声明变量的方式来实现这些。

一、C++中的声明、定义、初始化、赋值

1. 基本概念

  1. 声明:告诉编译器该变量存在,不分配内存空间,可以多次声明。
  2. 定义:只能定义一次,创建了一个类型对象,为类型对象分配内存空间。
  3. 初始化:在定义变量以后,系统为变量分配的空间内存储的值是不确定的,所以需要对这个空间进行初始化,以确保程序的安全性和确定性,给变量赋默认值。
  4. 赋值:变量在声明、定义或初始化完后对变量的值进行修改,即修改存储空间内的数据。

2. 使用说明

2.1 基本数据类型

  1. 基本数据类型的变量,一般在声明时就完成了定义。
  2. 对于普通变量,必须在初始化以后才能进行使用。
  3. 对于静态变量,如果不在代码中进行初始化,默认初始化为0值。
  4. 对于类中的成员,无论是静态的还是非静态的,都要显式的进行初始化,否则无法使用。

2.2 复合数据类型

2.2.1 数组
  1. 数组的声明和定义一般也会同时进行。
  2. 对于存放基本数据类型的普通的数组,如果没有做初始化,数组中元素的值都是未定义的。
  3. 对于存放基本数据类型的静态数组,如果没有做初始化,数组中的元素的值都是默认的,默认初始化为0值。
  4. 对于存放复杂数据类型的数组,无论是静态还是非静态,如果没有做初始化,数组中的元素的值都会默认初始化。
  5. 对于类中的存放基本数据类型的数组成员,如果是非静态数组,会进行默认初始化;如果是静态数组,需要手动进行初始化,否则无法使用。
  6. 对于类中的存放复杂数据类型的数组成员,如果是非静态数组,会进行默认初始化;如果是静态数组,需要手动进行初始化,否则无法使用。
2.2.2 其他复杂数据类型
  1. 对于有构造器的其他复杂数据类型,要看构造器的具体实现。
  2. 对于无构造器的非引用的其他复杂数据类型,和基本数据类型的状况相同。
  3. 对于引用,必须在声明时就被显式的初始化。

3. 代码示例

#include<iostream>
#include<string>
using namespace std;

int test = 10;

class Student {
public:
	int m = 10;
};

class Person {

public:
	static int c;
	static int arr[10];
	static Student arr1[10];
	static string arr2[10];
	static int* ip = &test;
};


int Person::c = 10;
int Person::arr[10] = {1,2,3};
Student Person::arr1[10] = {};
string Person::arr2[10] = {};
int* Person::ip = &test;

ostream& operator<<(ostream& out, Student& s) {
	out << "m:" << s.m << endl;;
	return out;
}

ostream& operator<<(ostream& out, Person& p) {
	out << "c:" << p.c << endl;;
	return out;
}

int main() {
	//普通基本数据类型需要显示初始化,否则不允许使用
	//int a;
	//cout << a << endl;
	//静态基本数据类型会被自动初始化
	static int b;
	cout << b << endl;
	//对于类中的成员,无论是静态的还是非静态的,都要显示的进行初始化,否则无法使用
	Person p1;
	cout << p1.c << endl;

	cout << "-------------------------------------------------" << endl;
	//对于存放基本数据类型的普通的数组,如果没有做初始化,数组中元素的值都是未定义的。
	//对于存放基本数据类型的静态数组,如果没有做初始化,数组中的元素的值都是默认的,默认初始化为0值。
	static int arr1[10];
	cout << arr1[1] << endl;
	//对于存放复杂数据类型的数组,无论是静态还是非静态,如果没有做初始化,数组中的元素的值都会默认初始化。
	static Person arr2[10];
	cout << "测试一" << arr2[0] << endl;
	string arr3[10];
	cout << "测试二" << arr3[0] << endl;
	int* arr4[10];
	cout << "测试三" << arr4[0] << endl;
	//对于类中的存放基本数据类型的数组成员,如果是非静态数组,会进行默认初始化;如果是静态数组,需要手动进行初始化,否则无法使用。
	Person p2;
	cout << "测试四" << p2.arr[1] << endl;
	//对于类中的存放复杂数据类型的数组成员,如果是非静态数组,会进行默认初始化;如果是静态数组,需要手动进行初始化,否则无法使用。
	Person p3;
	cout << "测试五" << p3.arr1[1] << endl;
	Person p4;
	cout << "测试五" << p4.arr2[1] << endl;
	cout << "-------------------------------------------------" << endl;
	//对于无构造器的非引用的其他复杂数据类型,和基本数据类型的状况相同。
	Person p5;
	cout<< p5.ip << endl;
	cin.get();
}

4. 声明和使用的顺序

  1. 在方法中声明和使用的变量,必须先声明再使用。
  2. 在类中,成员函数或成员变量的声明可以在写在使用之后。
  3. 在类中,如果用到了类外声明的内容,则声明必须写在类之前。

二、变量

为了区分不同的数据,在程序中一般会给它们起个唯一的名字,这就是所谓的变量。在C++中,变量其实就是记录了计算机内存中的一个位置标签,可以表示存放的数据对象。

1. 变量的声明

想要使用变量,必须先做声明,也就是告诉计算机要用到的数据叫什么名字,同时还要指明保存数据所需要的空间大小。比如:

int a;

这里包含两个信息:一个是变量的名字,叫做a,它对应着计算机内存中的一个位置;另一个是变量占据的空间大小,这是通过前面的int来指明的,表示我们需要足够的空间来存放一个整数类型(integer)数据。所以变量声明的标准语法可以写成:

数据类型 变量名;

变量名也可以有多个,用逗号分隔就可以。在C++中,可以处理各种不同类型的数据,这里的int就是最基本的一种数据类型(data type),表示一般的整数。

2. 变量的赋值

如果我们直接在代码中声明一个变量,然后打印输出的话就会报错,因为这个变量没有被初始化。也就是说,上面a这个变量现在可以表示内存中一个位置了,但是里面的数据是什么,就需要让a有一个初始值:

int a = 1;

上面这个操作叫做赋值。需要说明的是,这里等号=表示的是赋值操作,并不是数学上的等于。换句话说,我们还可以继续给a赋别的值:

int a = 1;
a = 2;

现在a的值就是2了。a的值可以改变,所以它叫做变量。

3. 标识符

每个变量都有一个名字,就是所谓的变量名。在C++中,变量、函数、类都可以有自己专门的名字,这些名字被叫做标识符。标识符由字母、数字和下划线组成;不能以数字开头;标识符是大小写敏感的,长度不限。所以下面的变量名都是合法而且不同的:

int b, B, B2, a1_B2;

此外,C++中还对变量命名有一些要求和约定俗成的规范:
①不能使用C++关键字;
②不能用连续两个下划线开头,也不能以下划线加大写字母开头;
③些被C++保留给标准库使用;
④函数体外的标识符,不能以下划线开头;
⑤要尽量有实际意义(不要定义a、b,而要定义name、age);
⑥变量名一般使用小写字母;
⑦自定义类名一般以大写字母开头;
⑧如果包含多个单词,一般用下划线分隔,或者将后面的单词首字母大写;
所谓的关键字,就是C++保留的一些单词,供语言本身的语法使用。包括:
在这里插入图片描述
在这里插入图片描述

4. 作用域

  1. 变量有了名字,那只要用这个名字就可以指代对应的数据。但是如果出现重名怎么办呢?在C++中,有作用域(scope)的概念,就是指程序中的某一段、某一部分。一般作用域都是以花括号{}作为分隔的,就像之前我们看到的函数体那样。同一个名字在不同的作用域中,可以指代不同的实体(变量、函数、类等等)。定义在所有花括号外的名字具有全局作用域(global scope),而在某个花括号内定义的名字具有块作用域。一般把具有全局作用域的变量叫做全局变量,具有块作用域的变量叫做局部变量
  2. 如果在嵌套作用域里出现重名,一般范围更小的局部变量会覆盖全局变量。如果要特意访问全局变量,需要加上双冒号::,指明是默认命名空间。

5. 常量

用变量可以灵活地保存数据、访问数据。不过有的时候,我们希望保存的数据不能更改,这种特殊的变量就被叫做常量。在C++中,有两种方式可以定义常量:

① 使用符号常量(不推荐):这种方式是在文件头用#define来定义常量,也叫作宏定义。跟#include一样,井号#开头的语句都是预处理语句,在编译之前,预处理器会查找程序中所有的ZERO”,并把它替换成0。这种宏定义的方式是保留的C语言特性,在C++中一般不推荐。

#define ZERO 0

② 使用const限定符:这种方式跟定义一个变量是一样的,只需要在变量的数据类型前再加上一个const关键字,这被称为限定符。

// 定义常量
const int Zero = 0;
// 不能修改常量值

const修饰的对象一旦创建就不能改变,所以必须初始化。const跟使用#define定义宏常量相比,const定义的常量有详细的数据类型,而且会在编译阶段进行安全检查,在运行时才完成替换,所以会更加安全和方便。

6. 注意

  1. C++是一种静态类型(statically typed)语言,需要在编译阶段做类型检查(type checking)。也就是说所有变量在创建的时候必须指明类型,而且之后不能更改。对于复杂的大型程序来说,这种方式更有助于提前发现问题、提高运行效率。
  2. 如果不给初始值,后面再赋值、再使用也是合法的;但一般不能不赋值、直接使用。因为在函数中定义的变量不被初始化,而在函数外部定义的变量会被默认初始化为0值。

三、基本数据类型

定义变量时,不可或缺的一个要素就是数据类型。本质上讲,这就是为了实现计算需求,我们必须先定义好数据的样式,告诉计算机这些数据占多大空间,这就是所谓数据类型的含义。C++支持丰富的数据类型,它内置了一套基本数据类型,也为我们提供了自定义类型的机制。接下来我们先介绍基本数据类型,主要包括算术类型空类型(void)。其中算术类型又包含了整型浮点型;而空类型不对应具体的值,只用在一些特定的场合,比如一个函数如果不返回任何值,我们可以让void作为它的返回类型。

1. 整型

1.1 常用整型

  • 整型(integral type)本质上来讲就是表示整数的类型。我们知道在计算机中,所有数据都是以二进制0和1来表示的,每个叫做一位(bit);计算机可寻址的内存最小单元是8位,也就是一个字节(Byte)。所以我们要访问的数据,都是保存在内存的一个个字节里的。一个字节能表示的最大数是256,这对于很多应用来讲显然是不够的。不同的需求可能要表示的数的范围也不一样,所以C++中定义了多个整数类型,它们的区别就在于每种类型占据的内存空间大小不同。
  • C++定义的基本整型包括char、short、int、long,和C++ 11新增的long long类型,此外特殊的布尔类型bool本质上也是整型。
    在这里插入图片描述
  • 在C++中对它们占据的长度定义比较灵活,这样不同的计算机平台就可以有自己的实现了(这跟C是一样的)。由于char和bool相对特殊,我们先介绍其它四种。C++标准中对它们有最小长度的要求,比如:①short类型至少为16位(2字节)②int至少2字节,而且不能比short短③long至少4字节,而且不能比int短④long long至少8字节,而且不能比long短。现在一般系统中,short和long都选择最小长度,也就是short为16位、long为32位、long long为64位;

1.2 无符号整型

整型默认是可正可负的,如果我们只想表示正数和0,那么所能表示的范围就又会增大一倍。以16位的short为例,本来表示的范围是-32768 ~ 32767,如果不考虑负数,那么就可以表示0 ~ 65535。C++中,short、int、long、long long都有各自的无符号版本的类型,只要定义时在类型前加上unsigned就可以。

short a = 32768;
//溢出,无法正常输出
cout << "a = " << a << endl;
//输出2,代表字节数
cout << "a的长度为:" << sizeof a << endl;

unsigned short a2 = 32768;
//正常输出32678
cout << "a2 = " << a2 << endl;
//输出2,代表字节数
cout << "a2的长度为:" << sizeof a2 << endl;

上面的代码可以测试无符号数表示的范围。需要注意,当数值超出了整型能表示的范围,程序本身并不会报错,而是会让数值回到能表示的最小值;这种情况叫做数据溢出(或者算术溢出),写程序时一定要避免。

由于类型太多,在实际应用中使用整型可以只考虑三个原则:①一般的整数计算,全部用int;②如果数值超过了int的表示范围,用long long;③确定数值不可能为负,用无符号类型(比如统计人数、销售额等);

1.3 char类型

char类型一般并不用在整数计算,它更重要的用途是表示字符(character)。最常用的字符编码集就是ASCII码,它用0~127表示了128个字符,这包括了所有的大小写字母、数字、标点符号、特殊符号以及一些计算机的控制符。比如字母A的编码是65,数字字符0的编码是48。

//输出65对应的字符
char ch = 65;
cout << "65对应的字符为:" << ch << endl;
//输出66
cout << "ch + 1:" << ch + 1 << endl;
//输出66对应的字符
char ch2 = ch + 1;
cout << "66对应的字符为:" << ch2 << endl;

在程序中如果使用char类型的变量,我们会发现,打印出来就是一个字符;而它的底层是一个整数,也可以做整数计算。

char类型用来表示整数时,到底是有符号还是无符号呢?之前的所有整型,默认都是有符号的,而char并没有默认类型,而是需要C++编译器根据需要自己决定。所以把char当做小整数时,有两种显式的定义方式:signed char 和 unsigned char;至于char定义出来的到底带不带符号,就看编译器的具体实现了。

C++还对字符类型进行了扩容,提供了一种宽字符类wchar_twchar_t会在底层对应另一种整型(比如short或者int),具体占几个字节要看系统中的实现。wchar_t会随着具体实现而变化,不够稳定;所以在C++11新标准中,还为Unicode字符集提供了专门的扩展字符类型:char16_tchar32_t,分别长16位和32位。
在这里插入图片描述

1.4 bool类型

在程序中,往往需要针对某个条件做判断,结果只有两种:成立和不成立;如果用逻辑语言来描述,就是真和假。真值判断是二元的,所以在C语言中,可以很简单地用1表示真,0表示假。

C++支持C语言中的这种定义,同时为了让代码更容易理解,引入了一种新的数据类型——布尔类型bool。bool类型只有两个取值:true和false,这样就可以非常明确地表示逻辑真假了。bool类型通常占用8位(1个字节)。

bool bl = true;
//输出1
cout << "bl = " << bl << endl;

2. 浮点型

跟整数对应,浮点数用来表示小数,主要有单精度float和双精度double两种类型,double的长度不会小于float。通常,float会占用4个字节(32位),而double会占用8个字节(64位)。此外,C++还提供了一种扩展的高精度类型long double,一般会占12或16个字节。

除了一般的小数,在C++中,还提供了另外一种浮点数的表示法,那就是科学计数法,也叫作E表示法。比如:5.98E24表示5.98×10的24次方9.11e-31表示9.11×10的-31次方

这就极大地扩展了我们能表示的数的范围。一般来讲,float至少有6位有效数字,double至少有15位有效数字。所以浮点类型不仅能表示小数,还可以表示(绝对值)非常大的整数。

3. 字面值常量

我们在给一个变量赋值的时候,会直接写一个整数或者小数,这个数据就是显式定义的常量值,叫做字面值常量。每个字面值常量也需要计算机进行保存和处理,所以也都是有数据类型的。字面值的写法形式和具体值,就决定了它的类型。

3.1 整型字面值

整型字面值就是我们直接写的一个整数,比如30。这是一个十进制数。而计算机底层是二进制的,所以还支持我们把一个数写成八进制和十六进制的形式。以0开头的整数表示八进制数;以0x或者0X开头的代表十六进制数。例如:

①30    十进制数
②036   八进制数
③0x1E  十六进制数 

这几个数本质上都是十进制的30,在计算机底层都是一样的。
在C++中,一个整型字面值,默认就是int类型,前提是数值在int能表示的范围内。如果超出int范围,那么就需要选择能够表示这个数的、长度最小的那个类型。

具体来说,对于十进制整型字面值,如果int不够那么选择long;还不够,就选择long long(不考虑无符号类型);而八进制和十六进制字面值,则会优先用无符号类型unsigned int,不够的话再选择long,之后依次是unsigned long、long long和unsigned long long。

这看起来非常复杂,很容易出现莫名其妙的错误。所以一般我们在定义整型字面值时,会给它加上一个后缀,明确地告诉计算机这个字面值是什么类型。

默认什么都不加,是int类型;
l或者L,表示long类型;
ll或者LL,表示long long类型;
u或者U,表示unsigned无符号类型;

我们一般会用大写L,避免跟数字1混淆;而u可以和LLL组合使用。例如9527uLL就表示这个数是unsigned long long类型。

3.2 浮点型字面值

前面已经提到,可以用一般的小数或者科学计数法表示的数,来给浮点类型赋值,这样的数就都是浮点型字面值。浮点型字面值默认的类型是double。如果我们希望明确指定类型,也可以加上相应的后缀:

f或者F,表示float类型
l或者L,表示long double类型

这里因为本身数值是小数或者科学计数法表示,所以L不会跟long类型混淆。

3.3 字符和字符串字面值

字符就是我们所说的字母、单个数字或者符号,字面值用单引号引起来表示。字符字面值默认的类型就是char,底层存储也是整型。

多个字符组合在一起,就构成了字符串。字符串字面值是一串字符,用双引号引起来表示。

字符串是字符的组合,所以字符串字面值的类型,本质上是char类型构成的数组(array)。关于数组的介绍,我们会在后面章节详细展开。

3.4 布尔字面值

布尔字面值非常简单,只有两个:true和false.

3.5 转义字符

有一类比较特殊的字符字面值,我们是不能直接使用的。在ASCII码中我们看到,除去字母、数字外还有很多符号,其中有一些本身在C++语法中有特殊的用途,比如单引号和双引号;另外还有一些控制字符。如果我们想要使用它们,就需要进行转义,这就是转义字符。C++中规定的转义字符有:
在这里插入图片描述

4. 赋值时涉及到的类型转换

我们在使用字面值常量给变量赋值时如果常量的值超出了变量类型能表示的范围,或者把一个浮点数赋值给整型变量,程序会进行自动类型转换。也就是说,程序会自动将一个常量值,转换成变量的数据类型,然后赋值给变量。

// 1. 整数值赋给bool类型
bool b = 25;    // b值为true,打印为1

// 2. bool类型赋值给算术整型
short s = false;    // s值为0

// 3. 浮点数赋给整数类型
int i = 3.14;    // i值为3

// 4. 整数值赋给浮点类型
float f = 10;    // f值为10.0,打印为10

// 5. 赋值超出整型范围
unsigned short us = 65536;    // us值为0
s = 32768;    // s值为-32768

转换规则可以总结如下:
①非布尔类型的算术值赋给布尔类型,初始值为0则结果为false , 否则结果为true 。
②布尔值赋给非布尔类型,初始值为false则结果为0,初始值为 true 则结果为1。
③浮点数赋给整数类型,只保留浮点数中的整数部分,会带来精度丢失。
④整数值赋给浮点类型,小数部分记为0。如果保存整数需要的空间超过了浮点类型的容量,可能会有精度丢失。
⑤给无符号类型赋值,如果超出它表示范围,结果是初始值对无符号类型能表示的数值总数取模后的余数。
⑥给有符号类型赋值,如果超出它表示范围,结果是未定义的(undefined)。此时,程序可能继续工作,也可能崩溃。

5. C++和Java易混淆处对比

C++和Java是两种当前使用范围很广泛的高级编程语言,两者有很多明显的不同,对此不再赘述。但对于相似之处的细微不同还需要进行细致区分,关于赋值和基本数据类型易混淆处的对比如下表所示:

C++Java
C++中的布尔类型在打印时会直接打印出1或0,且布尔类型可以其他基本数据类型进行相互转换,可以参加数学运算Java中的布尔类型是true和false,直接打印出的结果也是true和false,且该布尔类型不可进行计算,不可与其他基本数据类型相互转换
C++中可以将char类型的数据进行数学计算后赋给另外一个char类型的对象Java中char类型数据可以进行数学计算,但计算结果不可再赋值给char类型的对象
C++中float类型的数据可以使用科学计数法赋值Java中float不可以使用科学计数法赋值
C++左右值数据类型不同会自动根据左值的类型来对右值进行隐式类型转换Java左右值数据类型不同多数情况下需要手动对右值进行显式类型转换

四、复合数据类型

1. 数组

1.1 一维数组

1.1.1 数组的定义

数组的定义形式如下:

数据类型 数组名[元素个数];

首先需要声明类型,数组中所有元素必须具有相同的数据类型;
数组名是一个标识符;后面跟着中括号,里面定义了数组中元素的个数,也就是数组的长度;
元素个数也是类型的一部分,所以必须是确定的;

int a1[10];          // 定义一个数组a1,元素类型为int,个数为10

const int n = 4;
double a2[n];        // 元素个数可以是常量表达式

int i = 5;
//int a3[i];      // 错误,元素个数不能为变量

注意:没有通用的数组类型,所以上面的a1、a2的类型分别是int数组和double数组。这也是为什么我们把数组叫做复合数据类型。

1.1.2 数组的初始化
int a3[4] = {1,2,3,4};
float a4[] = {2.5, 3.8, 10.1};    // 正确,初始值说明了元素个数是3
short a5[10] = {3,6,9};    // 正确,指定了前三个元素,其余都为0
//long a6[2] = {3,6,9};    // 错误,初始值太多
//int a6[4] = a3;          // 错误,不能用另一个数组对数组赋值

注意:

  1. 对数组做初始化,要使用花括号{}括起来的数值序列;
  2. 如果做了初始化,数组定义时的元素个数可以省略,编译器可以根据初始化列表自动推断出来;
  3. 初始值的个数,不能超过指定的元素个数;
  4. 初始值的个数,如果小于元素个数,那么会用列表中的值初始化靠前的元素;剩余元素用默认值填充,整型的默认值就是0;
  5. 如果没有做初始化,数组中元素的值都是未定义的;这一点和普通的局部变量一致;
1.1.3 数组的访问

数组元素在内存中是连续存放的,它们排好了队之后就会有一个队伍中的编号,称为索引,也叫下标;通过下标就可以快速访问每个元素了,具体形式为:

数组名[元素下标]

这里也是用了中括号来表示元素下标位置,被称为下标运算符。比如a[2]就表示数组a中下标为2的元素,可以取它的值输出,也可以对它赋值。

int a[] = {1,2,3,4,5,6,7,8};
cout << "a[2] = " << a[2] << endl;    // a[2] = 3

a[2] = 36;
cout << "a[2] = " << a[2] << endl;    // a[2] = 36

注意:

  1. 数组的下标从0开始;
  2. 因此a[2]访问的并不是数组a的第2个元素,而是第三个元素;一个长度为10的数组,下标范围是0到9,而不是1到10;
  3. 合理的下标,不能小于0,也不能大于 (数组长度 - 1);否则就会出现数组下标越界;实际上数组没有对越界的行为作出限制,但如果越界访问会产生重大安全隐患,所以不要越界访问属组。
1.1.4 数组的大小

所有的变量,都会在内存中占据一定大小的空间;而数据类型就决定了它具体的大小。而对于数组这样的复合类型,由于每个元素类型相同,因此占据空间大小的计算遵循下面的简单公式:数组所占空间 = 数据类型所占空间大小 * 元素个数。这样一来,即使定义的时候没有指定数组元素个数,现在也可以计算得出了:

// a是已定义的数组
cout << "a所占空间大小:" << sizeof(a) << endl;
cout << "每个元素所占空间大小:" << sizeof(a[0]) << endl;
// 获取数组长度
int aSize = sizeof(a) / sizeof(a[0]);
cout << "数组a的元素个数:" << aSize << endl;

这里为了获取数组的长度,我们使用了sizeof运算符,它可以返回一个数据对象在内存中占用的大小(以字节为单位);数组总大小,除以每个数据元素的大小,就是元素个数。

1.1.5 数组的遍历

如果想要依次访问数组中所有的元素,就叫做遍历数组。我们通常使用for循环进行遍历:

// 获取数组长度
int aSize = sizeof(a) / sizeof(a[0]);

for (int i = 0; i < aSize; i++ )
{
	cout << "a[" << i << "] = " << a[i] << endl;
}

循环条件如果写一个具体的数,很容易出现下标越界的情况;而如果知道了数组长度,直接让循环变量i小于它就可以了。当然,这种写法还是稍显麻烦。C++ 11标准给我们提供了更简单的写法,就是之前介绍过的范围for循环:

for (int num: a )
{
	cout << num << endl;
}

当然,这种情况下就无法获取元素对应的下标了。

1.2 多维数组

1.2.1 数组的定义

之前介绍的数组只是数据最简单的排列方式。如果数据对象排列成的不是一队,而是一个方阵,那显然就不能只用一个下标来表示了。我们可以对数组进行扩展,让它从一维变成二维甚至多维。

int arr[3][4];         // 二维数组,有三个元素,每个元素是一个长度为4的int数组
int arr2[2][5][10];    // 三维数组

C++中本质上没有多维数组这种东西,所谓的多维数组,其实就是数组的数组。比如:
①二维数组int arr[3][4]表示:arr是一个有三个元素的数组,其中的每个元素都是一个int数组,包含4个元素;
③三维数组int arr2[2][5][10]表示:arr2是一个长度为2的数组,其中每个元素都是一个二维数组;这个二维数组有5个元素,每个元素都是一个长度为10的int数组;

一般最常见的就是二维数组。它有两个维度,第一个维度表示数组本身的长度,第二个表示每个元素的长度;一般分别把它们叫做行和列。实际上,对于二维数组arr,我们输出arr[0]时得到的是一个地址值。

1.2.2 数组的初始化

和普通的一维数组一样,多维数组初始化时,也可以用花括号括起来的一组数。使用嵌套的花括号可以让不同的维度更清晰:

数据类型 数组名[行数][列数] = {数据1, 数据2, 数据3,};

数据类型 数组名[行数][列数] = {
{数据11, 数据12, 数据13,}, 
{数据21, 数据22, 数据23,},};

注意:

  1. 内嵌的花括号不是必需的,因为数组中的元素在内存中连续存放,可以用一个花括号将所有数据括在一起;
  2. 初始值的个数,可以小于数组定义的长度,其它元素初始化为0值;这一点对整个二维数组和每一行的一维数组都适用;
  3. 如果省略嵌套的花括号,当初始值个数小于总元素个数时,会按照顺序依次填充(填满第一行,才填第二行);其它元素初始化为0值;
  4. 多维数组的维度,可以省略第一个,由编译器自动推断;即二维数组可以省略行数,但不能省略列数。
// 嵌套的花括号的初始化
int ia[3][4] = {
	{1,2,3,4},
	{5,6,7,8},
	{9,10,11,12}
};
//  只有一层花括号的初始化
int ia2[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };

// 部分初始化,其余补0
int ia3[3][4] = {
	{1,2,3},
	{5,6}
};
int ia4[3][4] = {1,2,3,4,5,6};
// 省略行数,自动推断
int ia5[][4] = {1,2,3,4,5};
1.2.3 数组的访问

可以用下标运算符来访问多维数组中的数据,数组的每一个维度,都应该有一个对应的下标。对于二维数组来说,就是需要指明行号列号,这相当于数据元素在二维矩阵中的坐标。

// 访问ia的第二行、第三个数据
cout << "ia[1][2] = " << ia[1][2] << endl;
// 修改ia的第一行、第二个数据
ia[0][1] = 19;

同样需要注意,行号和列号都是从0开始、到 (元素个数 - 1) 结束

1.2.2 数组的遍历

要想遍历数组,当然需要使用for循环,而且要扫描每一个维度。对于二维数组,我们需要对行和列分别进行扫描,这是一个双重for循环:

cout << "二维数组总大小:" << sizeof(ia) << endl;
cout << "二维数组每行大小:" << sizeof(ia[0]) << endl;
cout << "二维数组每个元素大小:" << sizeof(ia[0][0]) << endl;

// 二维数组行数
int rowCnt = sizeof(ia) / sizeof(ia[0]);
// 二维数组列数
int colCnt = sizeof(ia[0]) / sizeof(ia[0][0]);

for (int i = 0; i < rowCnt; i++)
{
	for (int j = 0; j < colCnt; j++)
	{
		cout << ia[i][j] << "\t";
	}
	cout << endl;
}

同样,这里利用了sizeof运算符:①行数 = 二维数组总大小 / 每行大小 ②列数 = 每行大小 / 每个元素大小。当然,也可以使用范围for循环:

for (auto & row : ia)
{
	for (auto num : row)
	{
		cout << num << "\t";
	}
	cout << endl;
}

这里的外层循环使用了auto关键字,这也是C++ 11新引入的特性,它可以自动推断变量的类型;后面的&是定义了一个引用.

2. 字符串

字符串我们并不陌生。之前已经介绍过,一串字符连在一起就是一个字符串,比如用双引号引起来的Hello World!就是一个字符串字面值。字符串其实就是所谓的纯文本,就是各种文字、数字、符号在一起表达的一串信息;所以字符串就是C++中用来表达和处理文本信息的数据类型。

2.1 标准库类型string

char*是一个指针,可以指向字符数组的起始内存地址。string本质上是一个类,类内部封装了char*来管理这个字符串,可以说string就是一个char*型的容器。string类型也定义在命名空间std中,使用它必须包含string头文件。

#include<string>
using namespace std;
2.1.1 字符数组(C风格字符串)

字符串就是一串字符的集合,本质上其实就是一个字符的数组。

在C语言中,确实是用char[]类型来表示字符串的;不过为了区分纯粹的字符数组和字符串,C语言规定:字符串必须以空字符结束。空字符的ASCII码为0,专门用来标记字符串的结尾,在程序中写作\0

// str1没有结尾空字符,并不是一个字符串
char str1[5] = {'h','e','l','l','o'};
// str2是一个字符串
char str2[6] = { 'h','e','l','l','o','\0'};
cout << "str1 = " << str1 << endl;
cout << "str2 = " << str2 << endl;

如果每次用到字符串都要这样定义,对程序员来说就非常不友好了。所以字符串可以用另一种更方便的形式定义出来,那就是使用双引号:

char str3[] = "hello";
//char str3[5] = "hello";    // 错误,"hello"的长度为6
cout << "str3 = " << str3 << endl;

这就是我们所熟悉的字符串字面值常量。这里需要注意的是,我们不需要再考虑末尾的空字符,编译器会自动帮我们补全;但真实的字符串的长度,依然要包含空字符,所以上面的字符串hello长度不是5、而是6。

所以,C++中的字符串字面值常量,为了兼容C依然定义为字符数组(char[])类型,这和string是两种不同类型;两者的区别,跟数组和vector的区别类似,char[]是更底层的类型。一般情况下,使用string会带来更多方便,也会更加安全。

2.1.2 构造函数

string的构造函数主要包括以下四种:

  1. string();:创建一个空的字符串 例如:string str;
  2. string(const char* s);:使用字符指针初始化,这种构造方式可以用字面值构造字符串。
  3. string(const string& str);:使用一个string对象初始化另一个string对象,其实就是拷贝构造。
  4. string(int n, char c);:使用n个字符c初始化 。
#include <string>
//string构造
void test01()
{
	string s1; //创建空字符串,调用无参构造函数
	cout << "str1 = " << s1 << endl;

	const char* str = "hello world";
	string s2(str); //把c_string转换成了string

	cout << "str2 = " << s2 << endl;

	string s3(s2); //调用拷贝构造函数
	cout << "str3 = " << s3 << endl;

	string s4(10, 'a');
	cout << "str3 = " << s3 << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}
2.1.3 赋值操作

string类提供了多种赋值操作,如下:

  1. string& operator=(const char* s);char*类型字符串赋值给当前的字符串。
  2. string& operator=(const string &s);:把字符串s赋给当前的字符串。
  3. string& operator=(char c);:字符赋值给当前的字符串。
  4. string& assign(const char *s);:把字符串s赋给当前的字符串。
  5. string& assign(const char *s, int n);:把字符串s的前n个字符赋给当前的字符串。
  6. string& assign(const string &s);:把字符串s赋给当前字符串。
  7. string& assign(int n, char c);:用n个字符c赋给当前字符串。
//赋值
void test01()
{
	string str1;
	str1 = "hello world";
	cout << "str1 = " << str1 << endl;

	string str2;
	str2 = str1;
	cout << "str2 = " << str2 << endl;

	string str3;
	str3 = 'a';
	cout << "str3 = " << str3 << endl;

	string str4;
	str4.assign("hello c++");
	cout << "str4 = " << str4 << endl;

	string str5;
	str5.assign("hello c++",5);
	cout << "str5 = " << str5 << endl;


	string str6;
	str6.assign(str5);
	cout << "str6 = " << str6 << endl;

	string str7;
	str7.assign(5, 'x');
	cout << "str7 = " << str7 << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}
2.1.4 字符串拼接

字符串可以做拼接操作,有以下几种方式可以进行字符串的拼接:

  1. string& operator+=(const char* str);:重载+=操作符
  2. string& operator+=(const char c);:重载+=操作符
  3. string& operator+=(const string& str);:重载+=操作符
  4. string& append(const char *s); :把字符串s连接到当前字符串结尾
  5. string& append(const char *s, int n);:把字符串s的前n个字符连接到当前字符串结尾
  6. string& append(const string &s);:同operator+=(const string& str)
  7. string& append(const string &s, int pos, int n);:字符串s中从pos开始的n个字符连接到字符串结尾
//字符串拼接
void test01()
{
	string str1 = "我";

	str1 += "爱玩游戏";

	cout << "str1 = " << str1 << endl;
	
	str1 += ':';

	cout << "str1 = " << str1 << endl;

	string str2 = "LOL DNF";

	str1 += str2;

	cout << "str1 = " << str1 << endl;

	string str3 = "I";
	str3.append(" love ");
	str3.append("game abcde", 4);
	//str3.append(str2);
	str3.append(str2, 4, 3); // 从下标4位置开始 ,截取3个字符,拼接到字符串末尾
	cout << "str3 = " << str3 << endl;
}
int main() {

	test01();

	system("pause");

	return 0;
}

注意:

  1. 两个字符串字面值常量不能相加;这是为了兼容C语言的字符数组,string类型为C++扩展的类型,利用该类型的+可以进行字符串拼接,但是字面值常量底层实际上是字符数组,字符数组不存在+操作符,也就无法进行拼接。
  2. 多个string对象和多个字符串字面值常量,可以连续相加;前提是按照左结合律,每次相加必须保证至少有一个string对象(不一定非要是左边是string对象,只要有一个就行)。
2.1.5 字符串比较
2.1.5.1 重载操作符

string类还提供几种用来做字符串比较的运算符,==!=用来判断两个字符串是否完全一样;而<><=>=则用来比较两个字符串的大小。这些都是关系型运算符的重载。

str1 = "hello";
str2 = "hello world!";
str3 = "hehehe";

str1 == str2;    // false
str1 < str2;     // true
str1 >= str3;     // true

字符串比较的规则为:

  1. 如果两个字符串长度相同,每个位置包含的字符也都相同,那么两者相等;否则不相等;
  2. 如果两个字符串长度不同,而较短的字符串每个字符都跟较长字符串对应位置字符相同,那么较短字符串小于较长字符串;
  3. 如果两个字符串在某一位置上开始不同,那么就比较这两个字符的ASCII码,比较结果就代表两个字符串的大小关系;
2.1.5.2 内置函数

string本身就定义了进行字符串比较的方法,具体介绍如下:

  1. int compare(const string &s) const; :与字符串s比较,s是字符串对象。
  2. int compare(const char *s) const;:与字符串s比较,s可以是字面值常量。
//字符串比较
void test01()
{

	string s1 = "hello";
	string s2 = "aello";

	int ret = s1.compare(s2);

	if (ret == 0) {
		cout << "s1 等于 s2" << endl;
	}
	else if (ret > 0)
	{
		cout << "s1 大于 s2" << endl;
	}
	else
	{
		cout << "s1 小于 s2" << endl;
	}

}

int main() {

	test01();

	system("pause");

	return 0;
}
2.1.6 字符串的常用函数
2.1.6.1 查找和替换

string类提供了字符串查找和替换的相关方法;find找到字符串后返回查找的第一个字符位置,找不到返回-1;replace在替换时,要指定从哪个位置起,多少个字符,替换成什么样的字符串;具体的方法介绍如下:

  1. int find(const string& str, int pos = 0) const;:查找str第一次出现位置,从pos开始查找。
  2. int find(const char* s, int pos = 0) const; :查找s第一次出现位置,从pos开始查找。
  3. int find(const char* s, int pos, int n) const; :从pos位置查找s的前n个字符第一次位置。
  4. int find(const char c, int pos = 0) const; :查找字符c第一次出现位置。
  5. int rfind(const string& str, int pos = npos) const;:查找str最后一次位置,从pos开始查找。
  6. int rfind(const char* s, int pos = npos) const;:查找s最后一次出现位置,从pos开始查找。
  7. int rfind(const char* s, int pos, int n) const;:从pos查找s的前n个字符最后一次位置。
  8. int rfind(const char c, int pos = 0) const; :查找字符c最后一次出现位置。
  9. string& replace(int pos, int n, const string& str); :替换从pos开始n个字符为字符串str。
  10. string& replace(int pos, int n,const char* s); :替换从pos开始的n个字符为字符串s。
//查找和替换
void test01()
{
	//查找
	string str1 = "abcdefgde";

	int pos = str1.find("de");

	if (pos == -1)
	{
		cout << "未找到" << endl;
	}
	else
	{
		cout << "pos = " << pos << endl;
	}
	

	pos = str1.rfind("de");

	cout << "pos = " << pos << endl;

}

void test02()
{
	//替换
	string str1 = "abcdefgde";
	str1.replace(1, 3, "1111");

	cout << "str1 = " << str1 << endl;
}

int main() {

	//test01();
	//test02();

	system("pause");

	return 0;
}
2.1.6.2 字符存取

string中单个字符存取方式有两种(存取表示可读可写):

  1. char& operator[](int n); :通过[]方式取字符。
  2. char& at(int n); :通过at方法获取字符。
void test01()
{
	string str = "hello world";

	for (int i = 0; i < str.size(); i++)
	{
		cout << str[i] << " ";
	}
	cout << endl;

	for (int i = 0; i < str.size(); i++)
	{
		cout << str.at(i) << " ";
	}
	cout << endl;


	//字符修改
	str[0] = 'x';
	str.at(1) = 'x';
	cout << str << endl;
	
}

int main() {

	test01();

	system("pause");

	return 0;
}
2.1.6.3 插入和删除

string字符串可以进行插入和删除字符操作:

  1. string& insert(int pos, const char* s); :插入字符串。
  2. string& insert(int pos, const string& str); :插入字符串。
  3. string& insert(int pos, int n, char c);:在指定位置插入n个字符c。
  4. string& erase(int pos, int n = npos);:删除从Pos开始的n个字符 。
//字符串插入和删除
void test01()
{
	string str = "hello";
	str.insert(1, "111");
	cout << str << endl;

	str.erase(1, 3);  //从1号位置开始3个字符
	cout << str << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}
2.1.6.4 子串

string可以从字符串中获取想要的子串,方法如下:

  1. string substr(int pos = 0, int n = npos) const; :返回由pos开始的n个字符组成的字符串
//子串
void test01()
{

	string str = "abcdefg";
	string subStr = str.substr(1, 3);
	cout << "subStr = " << subStr << endl;

	string email = "hello@sina.com";
	int pos = email.find("@");
	string username = email.substr(0, pos);
	cout << "username: " << username << endl;

}

int main() {

	test01();

	system("pause");

	return 0;
}

2.2 读取输入的字符串

程序中往往需要一些交互操作,如果想获取从键盘输入的字符串,可以使用多种方法。

2.2.1 使用输入操作符读取单词

标准库中提供了iostream,可以使用内置的cin对象,调用重载的输入操作符>>来读取键盘输入。

string str;
//  读取键盘输入,遇到空白符停止
cin >> str;
cout << str;

这种方式的特点是:忽略开始的空白符,遇到下一个空白符(空格、回车、制表等)就会停止。所以如果我们输入hello world,那么读取给str的只有hello:这相当于读取了一个单词。剩下的内容world其也没有丢,而是保存在了输入流的输入队列里。如果我们想读取更多的输入信息,就需要使用更多的string对象来获取:

string str1, str2;
cin >> str1 >> str2;
cout << str1 << str2 << endl;

这样,如果输入hello world,就可以输出helloworld。注意,此时的空白符只是忽略,并没有丢弃,依然存在于输入队列中

2.2.2 使用getline读取一行

如果希望直接读取一整行输入信息,可以使用getline函数来替代输入操作符。

string str3;
getline(cin, str3);
cout << "str3 = " << str3 << endl;

getline函数有两个参数:一个是输入流对象cin,另一个是保存字符串的string对象;它会一直读取输入流中的内容,直到遇到换行符为止,然后把所有内容保存到string对象中。所以现在可以完整读取一整行信息了。注意,此时的换行符已经丢弃,不然存在于输入队列中了

2.2.3 使用get读取字符

还有一种方法,是调用cin的get函数读取一个字符。

char ch;
ch = cin.get();         // 将捕获到的字符赋值给ch
cin.get(ch);            // 直接将ch作为参数传给get

有两种方式:

  1. 调用cin.get()函数,不传参数,得到一个字符赋给char类型变量;
  2. 将char类型变量作为参数传入,将捕获的字符赋值给它,返回的是istream对象

get函数还可以读取一行内容。这种方式跟getline很相似,也可以读取一整行内容,以回车结束。主要区别在于,它需要把信息保存在一个char[]类型的字符数组中,调用的是cin的成员函数:

// get读取一整行
char str4[20];
cin.get(str4, 20);
cout << "str4 = " << str4 << endl;

// get读取一个字符
cin.get();    // 先读取之前留下的回车符
cin.get();    // 再等待下一次输入

get函数同样需要传入两个参数:一个是保存信息的字符数组,另一个是字符数组的长度。

这里还要注意跟getline的另一个区别:键盘输入总是以回车作为结束的;getline会把最后的回车符丢弃,而get会将回车符保留在输入队列中。这样的效果是,下次再调用get试图读取一行数据时,会因为直接读到了回车符而返回空行。这就需要再次调用get函数,捕获下一个字符:

cin.get();    // 先读取之前留下的回车符
cin.get();    // 再等待下一次输入

这样就可以将之前的回车符捕获,从而为读取下一行做好准备。这也就解释了之前为什么要写两个cin.get():第一个用来处理之前保留在输入队列的回车符;第二个用来等待下一次输入,让窗口保持开启状态。

2.3 简单读写文件

实际应用中,我们往往会遇到读写文件的需求,这也是一种IO操作,整体用法跟命令行的输入输出非常类似。

C++的IO库中提供了专门用于文件输入的ifstream类和用于文件输出的ofstream类,要使用它们需要引入头文件fstream。ifstream用于读取文件内容,跟istream的用法类似;也可以通过输入操作符>>来读单词(空格分隔),通过getline函数来读取一行,通过get函数来读取一个字符:

ifstream input("input.txt");

// 逐词读取
string word;
while (input >> word) 
	cout << word << endl;

// 逐行读取
string line;
while (getline(input, line))
	cout << line << endl;

// 逐字符读取
char ch;
while (input.get(ch))
	cout << ch << endl;

类似地,写入文件也可以通过使用输出运算符<<来实现:

ofstream output("output.txt");
output << word << endl;

2.4 注意

C++中的string类型数据不能直接利用+和其他类型数据做拼接。

3. 结构体

实际应用中,我们往往希望把很多不同的信息组合起来,打包存储在一个单元中。比如一个学生的信息,可能包含了姓名、年龄、班级、成绩…这些信息的数据类型可能是不同的,所以数组和vector都无法完成这样的功能。C/C++中提供了另一种更加灵活的数据结构——结构体。结构体是用户自定义的复合数据结构,里面可以包含多个不同类型的数据对象。

3.1 结构体的声明

声明一个结构体需要使用struct关键字,具体形式如下:

struct 结构体名
{
类型1 数据对象1;
类型2 数据对象2;
类型3 数据对象3;};

结构体中数据对象的类型和个数都可以自定义,这为数据表达提供了极大的灵活性。结构体可以说是迈向面向对象世界中类概念的第一步。我们可以尝试定义这样一个学生信息结构体:

struct studentInfo 
{
	string name;
	int age;
	double score;
};

这个结构体中包含了三个数据对象:string类型的名字name,int类型的年龄age,以及double类型的成绩score。一般会把结构体定义在主函数外面,称为外部定义,这样可以方便外部访问。

3.2 结构体初始化

定义好结构之后,就产生了一个新的类型,叫做studentInfo。接下来就可以创建这种类型的对象,并做初始化了。

// 创建对象并初始化
studentInfo stu = {"张三", 20, 60.0};

结构体对象的初始化非常简单,跟数组完全一样:只要按照对应顺序一次赋值,逗号分隔,最后用花括号括起来就可以了。结构体还支持其它一些初始化方式:

struct studentInfo 
{
	string name;
	int age;
	double score;
}stu1, stu2 = {"小明", 18, 75.0};    // 定义结构体之后立即创建对象
// 使用列表初始化
studentInfo stu3{"李四", 22, 87};
// 使用另一结构体对象进行赋值
studentInfo stu4 = stu2;

需要注意:

  1. 创建结构体变量对象时,可以直接用定义好的结构体名作为类型;相比C语言中的定义,这里省略了关键字struct
  2. 不同的初始化方式效果相同,在不同位置定义的对象作用域不同;
  3. 如果没有赋初始值,那么所有数据将被初始化为默认值;算术类型的默认值就是0;
  4. 一般在代码中,会将结构体的定义和对象的创建分开,便于理解和管理

3.3 访问结构体中数据

访问结构体变量中的数据成员,可以使用成员运算符(点号.),后面跟上数据成员的名称。例如stu.name就可以访问stu对象的name成员。

cout << "学生姓名:" << stu.name << "\t年龄:" << stu.age << "\t成绩:" << stu.score << endl;

这种访问内部成员的方式非常经典,后面要讲到的类的操作中,也会用这种方式访问自己的成员函数。

3.4 结构体数组

可以把结构体和数组结合起来,创建结构体的数组。顾名思义,结构体数组就是元素为结构体的数组,它的定义和访问跟普通的数组完全一样。

// 结构体数组
studentInfo s[2] = {
	{"小红", 18, 92},
	{"小白", 20, 82}
};

cout << "学生姓名:" << s[0].name << "\t年龄:" << s[0].age << "\t成绩:" << s[0].score << endl;
cout << "学生姓名:" << s[1].name << "\t年龄:" << s[1].age << "\t成绩:" << s[1].score << endl;

4. 枚举类

实际应用中,经常会遇到某个数据对象只能取有限个常量值的情况,比如一周有7天,一副扑克牌有4种花色等等。对于这种情况,C++提供了另一种批量创建符号常量的方式,可以替代const。这就是枚举类型enum。

4.1 枚举类型定义

枚举类型的定义和结构体非常像,需要使用enum关键字。

// 定义枚举类型
enum week
{
	Mon, Tue, Wed, Thu, Fri, Sat, Sun
};

与结构体不同的是,枚举类型内只有有限个名字,它们都各自代表一个常量,被称为枚举量。需要注意的是:

  1. 默认情况下,会将整数值赋给枚举量;
  2. 枚举量默认从0开始,每个枚举量依次加1;所以上面week枚举类型中,一周七天枚举量分别对应着0~6的常量值;
  3. 可以通过对枚举量赋值,显式地设置每个枚举量的值;如果对中间的某个枚举量设置了指定的整型值,该枚举量后面的枚举量的值会以该值为基础进行加1;

4.2 使用枚举类型

使用枚举类型也很简单,创建枚举类型的对象后,只能将对应类型的枚举量赋值给它;如果打印它的值,将会得到对应的整数。

week w1 = Mon;
week w2 = Tue;
//week w3 = 2;    // 错误,类型不匹配
week w3 = week(3);    // int类型强转为week类型后赋值

cout << "w1 = " << w1 << endl;
cout << "w2 = " << w2 << endl;
cout << "w3 = " << w3 << endl;

注意:

  1. 如果直接用一个整型值对枚举类型赋值,将会报错,因为类型不匹配;
  2. 可以通过强制类型转换,将一个整型值赋值给枚举对象;
  3. 最初的枚举类型只有列出的值是有效的;而现在C++通过强制类型转换,允许扩大枚举类型合法值的范围。不过一般使用枚举类型要避免直接强转赋值。

5. 指针

计算机中的数据都存放在内存中,访问内存的最小单元是字节(byte)。所有的数据,就保存在内存中具有连续编号的一串字节里。
在这里插入图片描述
指针顾名思义,是指向另外一种数据类型的复合类型。指针是C/C++中一种特殊的数据类型,它所保存的信息,其实是另外一个数据对象在内存中的地址。通过指针可以访问到指向的那个数据对象,所以这是一种间接访问对象的方法。

5.1 指针的定义

指针的定义语法形式为:

类型* 指针变量;

这里的类型就是指针所指向的数据类型,后面加上星号*,然后跟指针变量的名称。指针在定义的时候可以不做初始化。相比一般的变量声明,看起来指针只是多了一个星号*而已。例如:

int* p1;      // p1是指向int类型数据的指针
long* p2;     // p2是指向long类型数据的指针
cout << "p1在内存中长度为:" << sizeof(p1) << endl;
cout << "p2在内存中长度为:" << sizeof(p2) << endl;

p1、p2就是两个指针,分别指向int类型和long类型的数据对象。指针的本质,其实就是一个整数表示的内存地址,它本身在内存中所占大小跟系统环境有关,而跟指向的数据类型无关。64位编译环境中,指针统一占8个字节;若是32位系统则占4字节。

5.1.1 获取对象地址给指针赋值

指针保存的是数据对象的内存地址,所以可以用地址给指针赋值;获取对象地址的方式是使用取地址操作符&

int a = 12;
int b = 100;

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

int* p = &b;           // p是指向b的指针
p = &a;    // p指向了a
cout << "p = " << p << endl;

把指针当做一个变量,可以先指向一个对象,再指向另一个不同的对象。

5.1.2 通过指针访问对象

指针指向数据对象后,可以通过指针来访问对象。访问方式是使用解引用操作符*

p = &a;    // p是指向a的指针
cout << "p指向的内存中,存放的值为:" << *p << endl;
*p = 25;    // 将p所指向的对象(a),修改为25
cout << "a = " << a << endl;

在这里由于p指向了a,所以*p可以等同于a。

5.2 无效指针、空指针和void*指针

5.2.1 无效指针

定义一个指针之后,如果不进行初始化,那么它的内容是不确定的。如果这时把它的内容当成一个地址去访问,就可能访问的是不存在的对象;更可怕的是,如果访问到的是系统核心内存区域,修改其中内容会导致系统崩溃。这样的指针就是无效指针,也被叫做野指针。

	int* p1;
	//*p1 = 100;    // 危险!指针没有初始化,是无效指针

指针非常灵活非常强大,但野指针非常危险。所以建议使用指针的时候,一定要先初始化,让它指向真实的对象。

5.2.2 空指针

如果先定义了一个指针,但确实还不知道它要指向哪个对象,这时可以把它初始化为空指针。空指针不指向任何对象,它一般在程序中用来做判断,看一个指针是否指向了数据对象。

int* np = nullptr;    // 空指针字面值
np = NULL;      // 预处理变量
np = 0;         // 0值

int zero = 0;
//np = zero;      // 错误,int变量不能赋值给指针

cosnt int cos = 0;
//np = cos;      // 错误,int常量不能赋值给指针

cout << "np = " << np << endl;           // 输出0地址
//cout << "*np = " << *np << endl;       // 错误,不能访问0地址的内容

空指针有几种定义方式:

  1. 使用字面值nullptr,这是C++ 11 引入的方式,推荐使用;
  2. 使用预处理变量NULL,这是老版本的方式;
  3. 直接使用0值;
  4. 另外注意,不能直接用整型变量和常量给指针赋值,即使值为0也不行

所以可以看出,空指针所保存的其实就是0值,一般把它叫做0地址;但是C/C++中的这个0地址并不一定对应当前机器的0地址,而是会把0地址解释为一个不表示任何数据的不被使用的内存地址,具体指向哪里跟系统本身的实现有关;如果本身指向的就是内存中的0地址,这个地址由于有特殊的含义也是不允许访问的。

5.2.3 void * 指针

一般来说,指针的类型必须和指向的对象类型匹配,否则就会报错。不过有一种指针比较特殊,可以用来存放任意对象的地址,这种指针的类型是void*。

int i = 10;
string s = "hello";

void* vp = &i;
vp = &s;
cout << "vp = " << vp << endl;
cout << "vp的长度为: " << sizeof(vp) << endl;
//cout << "*vp = " << *vp << endl;    // 错误,不能通过void *指针访问对象

void* 指针表示只知道保存了一个地址,至于这个地址对应的数据对象是什么类型并不清楚。所以不能通过void指针访问对象;一般 void 指针只用来比较地址、或者作为函数的输入输出。

5.3 指向指针的指针

指针本身也是一个数据对象,也有自己的内存地址。所以可以让一个指针保存另一个指针的地址,这就是指向指针的指针,有时也叫二级指针;形式上可以用连续两个的星号**来表示。类似地,如果是三级指针就是***,表示指向二级指针的指针。
在这里插入图片描述

int i = 1024;
int* pi = &i;        // pi是一个指针,指向int类型的数据
int** ppi = &pi;     // ppi是一个二级指针,指向一个int* 类型的指针

cout << "pi = " << pi << endl;
cout << "* pi = " << * pi << endl;
cout << "ppi = " << ppi << endl;
cout << "* ppi = " << * ppi << endl;
cout << "** ppi = " << ** ppi << endl;

//必须一级一级的来,不能一次用两个&,这就乱套了,没理解底层的实现
int a = 0;
int** p = &&a;

如果需要访问二级指针所指向的最原始的那个数据,应该做两次解引用操作。

5.4 指针和const

指针可以和const修饰符结合,这可以有两种形式:一种是指针指向的是一个常量;另一种是指针本身是一个常量。

5.4.1 指向常量的指针

指针指向的是一个常量,所以只能访问数据,不能通过指针对数据进行修改。不过指针本身是变量,可以指向另外的数据对象。这时应该把const加在类型前。

const int c = 10, c2 = 56;
//int* pc = &c;        // 错误,类型不匹配
const int* pc = &c;    // 正确,pc是指向常量的指针,类型为const int *

pc = &c2;            // pc可以指向另一个常量
int i = 1024;
pc = &i;             // pc也可以指向变量
//*pc = 1000;          // 错误,不能通过pc更改数据对象

这里发现,pc是一个指向常量的指针,但其实把一个变量i的地址赋给它也是可以的;编译器只是不允许通过指针pc去间接更改数据对象。

5.4.2 指针常量(const指针)

指针本身是一个数据对象,所以也可以区分变量和常量。如果指针本身是一个常量,就意味它保存的地址不能更改,也就是它永远指向同一个对象;而数据对象的内容是可以通过指针改变的。这种指针一般叫做指针常量。指针常量在定义的时候,需要在星号*后、标识符前加上const。

const int c = 10, c2 = 56;
int i = 1024;
int* const cp = &i;
*cp = 2048;             // 通过指针修改对象的值
cout << "i = " << i << endl;

//cp = &c;              // 错误,不可以更改cp的指向

const int* const ccp = &c;    // ccp是一个指向常量的常量指针

这里也可以使用两个const,定义的是指向常量的常量指针。也就是说,ccp指向的是常量,值不能改变;而且它本身也是一个常量,指向的对象也不能改变。

5.5 指针和数组

在这里插入图片描述

5.5.1 数组名

用到数组名时,编译器一般都会把它转换成指针,这个指针就指向数组的第一个元素。所以我们也可以用数组名来给指针赋值。

int arr[] = {1,2,3,4,5};
cout << "arr = " << arr << endl;
cout << "&arr[0] = " << &arr[0] << endl;

int* pia = arr;      // 可以直接用数组名给指针赋值
cout << "* pia = " << *pia << endl;    // 指针指向的数据,就是arr[0]

也正是因为数组名被认为是指针,所以不能直接使用数组名对另一个数组赋值,数组也不允许这样的直接拷贝:

int arr[] = {1,2,3,4,5};
//int arr2[5] = arr;    // 错误,数组不能直接拷贝
5.5.2 指针运算

如果对指针pia做加1操作,我们会发现它保存的地址直接加了4,这其实是指向了下一个int类型数据对象:

pia + 1;        // pia + 1 指向的是arr[1]
*(pia + 1);        // 访问 arr[1]

所谓的指针运算,就是直接对一个指针加/减一个整数值,得到的结果仍然是指针。新指针指向的数据元素,跟原指针指向的相比移动了对应个数据单位。

5.5.3 指针和数组下标

我们知道,数组名arr其实就是指针。这就带来了非常有趣的访问方式:

* arr;     // arr[0]
*(arr + 1);     // arr[1]

这是通过指针来访问数组元素,效果跟使用下标运算符arr[0]、arr[1]是一样的。进而我们也可以发现,遍历元素所谓的范围for循环,其实就是让指针不停地向后移动依次访问元素。

5.5.4 指针数组和数组指针

指针和数组这两种类型可以结合在一起,这就是指针数组和数组指针。

  1. 指针数组:一个数组,它的所有元素都是相同类型的指针;
  2. 数组指针:一个指针,指向一个数组的指针;
int arr[] = {1,2,3,4,5};
int* pa[5];        // 指针数组,里面有5个元素,每个元素都是一个int指针
int(* ap)[5];      // 数组指针,指向一个int数组,数组包含5个元素

cout << "指针数组pr的大小为:" << sizeof(pa) << endl;    // 40
cout << "数组指针ap的大小为:" << sizeof(ap) << endl;    // 8

pa[0] = arr;           // pa中第一个元素,指向arr的第一个元素
pa[1] = arr + 1;       // pa中第二个元素,指向arr的第二个元素

ap = &arr;             // ap指向了arr整个数组

cout << "arr =" << arr << endl;					//arr实际上就是int型的指针,即int* 
cout << "* arr =" << *arr << endl;              //arr解引用,得到arr[0]
cout << "arr + 1 =" << arr + 1 << endl;			//实际上是指向arr中第二个元素的指针,也是int型的指针,即int*
cout << "ap =" << ap << endl;					//ap是数组指针,但是实际上ap的值和arr的值是相同的,只是类型不一样
cout << "* ap =" << *ap << endl;                //ap解引用,得到的是arr数组,也可以说是int*类型的指针
cout << "ap + 1 =" << ap + 1 << endl;         	//输出arr之后的地址   

这里可以看到,指向数组arr的指针ap,其实保存的也是arr第一个元素的地址。arr类型是int *,指向的就是arr[0];而ap类型是int (*) [5],指向的是整个arr数组。所以arr + 1,得到的是arr[1]的地址;而ap + 1,就会跨过整个arr数组。

6. 引用

我们可以在C++中为数据对象另外起一个名字,这叫做引用(reference)。

6.1 引用的用法

在做声明时,我们可以在变量名前加上&符号,表示它是另一个变量的引用。引用必须被初始化。

int a = 10;
int& ref = a;          // ref是a的引用
//int& ref2;             // 错误,引用必须初始化
cout << "ref = " << ref << endl;            // ref等于a的值

cout << "a的地址为:" << &a << endl;
cout << "ref的地址为:" << &ref << endl;    // ref和a的地址完全一样

引用本质上就是一个别名,它本身不是数据对象,所以本身不会存储数据,而是和初始值绑定(bind)在一起,绑定之后就不能再绑定别的对象了。定义了引用之后,对引用做的所有操作,就像直接操作绑定的原始变量一样。所以,引用也是一种间接访问数据对象的方式。

ref = 20;                  // 更改ref相当于更改a
cout << "a = " << a << endl;

int b = 26;
ref = b;                  // ref没有绑定b,而是把b的值赋给了ref绑定的a
cout << "a的地址为:" << &a << endl;
cout << "b的地址为:" << &b << endl;
cout << "ref的地址为:" << &ref << endl;
cout << "a = " << a << endl;

当然,既然是别名,那么根据这个别名再另起一个别名也是可以的:

// 引用的引用
int& rref = ref;
cout << "rref = " << rref << endl;
cout << "a的地址为:" << &a << endl;
cout << "ref的地址为:" << &ref << endl;
cout << "rref的地址为:" << &rref << endl;

引用的引用,是把引用作为另一个引用的初始值,其实就是给原来绑定的对象又绑定了一个别名,这两个引用绑定的是同一个对象。要注意,引用只能绑定到对象上,而不能跟字面值常量绑定;也就是说,不能把一个字面值直接作为初始值赋给一个引用。而且,引用本身的类型必须跟绑定的对象类型一致。

//int& ref2 = 10;          // 错误,不能创建字面值的引用
double d = 3.14;
//int& ref3 = d;           // 错误,引用类型和原数据对象类型必须一致

6.2 对常量的引用

可以把引用绑定到一个常量上,这就是对常量的引用。很显然,对常量的引用是常量的别名,绑定的对象不能修改,所以也不能做赋值操作:

const int zero = 0;
//int& cref = zero;        // 错误,不能用普通引用去绑定常量
const int& cref = zero;    // 常量的引用
//cref = 10;                 // 错误,不能对常量赋值

对常量的引用有时也会直接简称常量引用。因为引用只是别名,本身不是数据对象;所以这只能代表对一个常量的引用,而不会像常量指针那样引起混淆。

常量引用和普通变量的引用不同,它的初始化要求宽松很多,只要是可以转换成它指定类型的所有表达式,都可以用来做初始化。

const int& cref2 = 10;     // 正确,可以用字面值常量做初始化

int i = 35;
const int& cref3 = i;      // 正确,可以用一个变量做初始化
i = 40;
cout << cref3 << endl;// 如果更改了i,该常量引用的值随之更改

double d = 3.14;
const int& cref4 = d;      // 正确,d会先转成int类型,引用绑定的是一个临时量

这样一来,常量引用和变量引用,都可以作为一个变量的别名,区别在于不能用常量引用去修改对象的值。

int var = 10;
int& r1 = var;
const int& r2 = var;
r1 = 25;
//r2 = 35;                  // 错误,不能通过const引用修改对象值

6.3 指针和引用

常量引用和指向常量的指针,有很类似的地方:它们都可以绑定/指向一个常量,也可以绑定/指向一个变量;但不可以去修改对应的变量对象。所以很明显,指针和引用有很多联系。

6.3.1 引用和指针常量

事实上,引用的行为,非常类似于指针常量,也就是只能指向唯一的对象、不能更改的指针。

int a = 10;

// 引用的行为,和指针常量非常类似
int& r = a;
int* const p = &a;

r = 20;
*p = 30;

cout << "a = " << a << endl;
cout << "a的地址为:" << &a << endl;

cout << "r = " << r << endl;
cout << "r的地址为:" << &r << endl;

cout << "*p = " << *p << endl;
cout << "p = " << p << endl;

可以看到,所有用到引用r的地方,都可以用p替换;所有需要获取地址&r的地方,也都可以用p替换。这也就是为什么把操作符,叫做解引用操作符。

6.3.2 指针的引用

指针本身也是一个数据对象,所以当然也可以给它起别名,用一个引用来绑定它。比如:pref是指针ptr的引用,所以下面所有的操作,pref就等同于ptr。

int i = 56, j = 28;;
int* ptr = &i;    		// ptr是一个指针,指向int类型对象
int*& pref = ptr;   	// pref是一个引用,绑定指针ptr

pref = &j;           	// 将指针ptr指向j
*pref = 20;         	// 将j的值变为20

可以有指针的引用、引用的引用,也可以有指向指针的指针;但由于引用只是一个别名,不是实体对象,所以不存在指向引用的指针。

int i = 56
int& ref = i;
//int&* rptr = &ref;     // 错误,不允许使用指向引用的指针
int* rptr = &ref;        // 事实上就是指向了i
6.3.3 引用的本质

引用类似于指针常量,但不等同于指针常量。它是对指针的一种伪装。

指针常量引用
指针常量本身还是一个数据对象,它保存着另一个对象的地址,而且不能更改而引用就是别名,它会被编译器直接翻译成所绑定的原始变量;所以我们会看到,引用和原始对象的地址是一样,引用并没有额外占用内存空间;这也是为什么不会有指向引用的指针

引用的本质,只是C++引入的一种语法糖。指针是C语言中最灵活、最强大的特性;引用所能做的,其实指针全都可以做。但是指针同时又令人费解、充满危险性,所以C++中通过引用来代替一些指针的用法。后面在函数部分,我们会对此有更深刻的理解。

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值