第二章 C++的学习
为查看输出,必须在程序的最后加上一些代码:
cin.get();
//读取下一次键击,因此上述语句让程序等待,直到按下了Enter键
C语言输入和输出
如果已经使用过C语言进行编程,则看到cout函数(而不是printf()函数)时可能会诧异。事实上C++能够使用printf()、scanf()和其他所有标准C输入和输出函数,只需要包含常规C语言的stdio.h文件。使用C++的输入工具,它们在C版本的基础上作出了很多改进。
注释,由前缀//标识
预处理器编译指令#include
函数头:int main()
编译指令 using namespace
函数体,用{和}括起
使用C++的cout工具显示消息的语句
结束main()函数的return语句。
头文件命令的约定
头文件类型 | 约定 | 示例 | 说明 |
---|
C++旧式风格 | 以.h结尾 | iostream.h | C++程序可以使用 | C旧式风格 | 以.h结尾 | math.h | C、C++程序都可以使用 | C++新式风格 | 没有扩展名 | iostream | C++程序可以使用,使用namespace std | 转换后的C | 加上前缀c,没有扩展名 | cmath | C++程序可以使用,可以使用不是C的特性 |
控制符endl
cout << endl;
//endl是一个特殊的C++符号,表示一个重要的概念:重起一行。在输出流中插入endl将导致屏幕光标移到下一行开头。
诸如endl等对于cout来说有特殊含义的特殊符号称为控制符(manipulator)。和cout一样,endl也是在头文件iostream中定义的,且位于名称空间std中。
打印字符串时,cout不会自动移动到下一行。第一条cout语句将光标留在输出字符串的后面。每条cout语句的输出从前一个输出的末尾开始。
cout << "The Good,the";
cout << "Bad, ";
cout << "and the Ukulele";
cout << endl;
//输出结果
//The Good,the Bad,and the Ukulele
换行符
C++还提供了另一种在输出中指示换行的旧式方法:C语言符号\n
cout << "What's next? \n";
\n被视为一个字符,名为换行符。
显示字符串时,在字符串中包含换行符,而不是在末尾加上endl,可减少输入量:
cout << "pluto is a dwarf planet.\n";
cout << "pluto is a dwarf planet."<< endl;
另一方面,如果要生成一个空行,则两种方法的输入量相同,但对于大多数人而言,输入endl更方便。
cout << "\n";
cout << endl;
C++语句
#include <iostream>
using namespace std;
int main()
{
int carrots;
carrots = 25;
cout << "I have ";
cout << carrots;
cout << " carrots.";
cout << endl;
carrots = carrots - 1;
cout << "Crunch,crunch.Now I have "<< carrots << " carrots." << endl;
return 0;
}
结果输出:
I have 25 carrots.
Crunch,crunch.Now I have 24 carrots.
cout和printf()
如果已经习惯了C语言的printf(),可能觉得cout看起来很奇怪。程序员甚至可能固执地坚持使用printf()。但与使用所有转换说明的printf()相比,cout的外观一点儿也不奇怪。更重要的是,cout还有明显的优点。它能够识别类型的功能表明,其设计更灵活、更好用。另外,它是可扩展的。也就是说,可以重新定义<<运算符,使cout能够识别和显示所开发的新数据类型。如果喜欢printf()提供的细致的控制功能,可以使用更高级的cout来获得相同的效果。
其他C++语句
#include <iostream>
using namespace std;
int main()
{
int carrots;
cout << "How many carrots do you have?" << endl;
cin >> carrots;
cout << "Here are two more. ";
carrots = carrots + 2;
cout << "Now you have " << carrots << " carrots." << endl;
return 0;
}
输出结果:
How many carrots do you have?
32
Here are two more. Now you have 34 carrots.
程序调整:
如果您发现在以前的程序清单中需要添加cin.get(),则在这个程序清单中,需要添加两条cin.get()语句,这样才能在屏幕上看到输出。第一条cin.get()语句在您输入数字并按Enter键时读取输入,而第二条cin.get()语句让程序暂停,直到您按Enter键。
使用cout进行拼接
cout << "Now you have " << carrots << " carrots." << endl;
类简介
类是用户定义的一种数据类型。要定义类,需要描述它能够表示什么信息和可对数据执行哪些操作。类之于对象就像类型之于变量。也就是说,类定义描述的是数据格式及其用法,而对象则是根据格式规范的实体。换句话说,如果说类就好比所有著名演员,则对象就好比某个著名的演员。
注意:类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
函数
由于函数用于创建C++程序的模块,对C++的OOP定义至关重要。
有返回值的函数
有返回值的函数将生成一个值,而这个值可赋给变量或在其他表达式中使用。例如,标准C/C++库包含一个名为sqrt()的函数,它返回平方根。
double x;
x = sqrt(6.25); //return the value 2.5 and assigns it to x
表达式sqrt(6.25)将调用sqrt()函数。表达式sqrt(6.25)被称为函数调用。
被调用的函数叫做被调用函数(called function),包含函数调用的函数叫做调用函数(calling function)。
注意:C++程序应当为程序中使用的每个函数提供原型。
在程序中使用sqrt()时,也必须提供原型。可以用两种方法来实现:
- 在源代码文件中输入函数原型;
- 包含头文件cmath(老系统为math.h),其中定义了原型。
第二种方法更好,因为头文件更有可能使原型正确。对于C++库中的每个函数,都在一个或多个头文件中提供了原型。请通过手册或在线帮助查看函数描述来确定应使用哪个头文件。例如:sqrt()函数的说明指出,应使用cmath头文件。(同样,可能必须使用老式的头文件math.h,它可用于C和C++程序中。)
不要混淆函数原型和函数定义。可以看出,原型只描述函数接口。也就是说,它描述的是发送给函数的信息和返回的信息。而定义中包含了函数的代码,如计算平方根的代码。
C和C++将库函数的这两项特性(原型和定义)分开了。库文件中包含了函数的编译代码,而头文件中则包含了原型。
#include <iostream>
#include <cmath> //or math.h
using namespace std;
int main()
{
double area;
cout << "Enter the floor area,in square feet,of your home:";
cin >> area;
double side;
side = sqrt(area);
cout << "That's the equivalent of a square " << side << " feet to the side." << endl;
cout << "How fascinating!" << endl;
return 0;
}
运行结果:
Enter the floor area,in square feet,of your home:
1536
That's the equivalent of a square 39.1918 feet to the side.
How fascinating!
C++允许在程序 的任何地方声明新变量,因此sqrt.cpp在要使用side时才声明它。
C++还允许在创建变量时对它进行赋值:
double side = sqrt(area);
这个过程叫做初始化(initialization),cin知道如何将输入流中的信息转换为double类型,cout知道如何将double类型插入到输出流中。这些对象都很智能化。
函数变体
有些函数需要多项信息。这些函数使用多个参数,参数之间用逗号分开。
例如,数学函数pow()接受两个参数,返回值为以第一个参数为底,第二个参数为指数的幂。
该函数的原型:
double pow(double,double); //prototype of a function with two arguments
要计算5的8次方:
answer = pow(5.0,8.0);
另外一些函数不接受任何参数。例如,有一个C库(cstdlib或stdlib.h头文件相关的库)包含一个rand()函数,该函数不接受任何参数,并返回一个随机整数。
该函数的原型如下:
int rand(void); //prototype of a function that takes no arguments
关键字void明确指出,该函数不接受任何参数。如果省略void,让括号为空,则C++将其解释为一个不接受任何参数的隐式声明。
可以这样使用该函数:
myGuess = rand(); //function call with no arguments
用户定义函数
#include <iostream>
using namespace std;
void simon(int); //function protype for simon()
int main()
{
simon(3);
cout << "Pick an integer: " ;
int count;
cin >> count;
simon(count);
cout << "Done!" << endl;
return 0;
}
void simon(int n) //define the simon() function
{
cout << "Simon says touch yours toes " << n << "times." << endl;
}
运行结果:
Simon says touch yours toes 3 times.
Pick an integer: 512
Simon says touch yours toes 512 times.
Done!
函数格式
simon()函数的定义与main()的定义采用的格式相同。首先,有一个函数头,然后是花括号中的函数体。可以把函数的格式统一为如下的情形:
type functionname(argumentlist)
{
statements
}
C++不允许将函数定义嵌套在另一个函数定义中。每个函数定义都是独立的,所有函数的创建都是平等的。
return 0;
//通常的约定是,退出值(返回值)为0则意味着程序运行成功,为非零则意味着存在问题。
有返回值的函数
#include <iostream>
using namespace std;
int stonetolb(int); //function prototype
int main()
{
int stone;
cout << "Enter the weight in stone: ";
cin >> stone;
int pounds = stonetolb(stone);
cout << stone << " stone = ";
cout << pounds << " pounds. " << endl;
return 0;
}
int stonetolb(int sts)
{
return 14 * sts;
}
运行结果:
Enter the weight in stone: 15
15 stone = 210 pounds.
如果返回表达式的值很麻烦,可以采取更复杂的方式:
int stonetolb(int sts)
{
int pounds = 14 * sts;
return pounds;
}
通常,在可以使用一个简单常量的地方,都可以使用一个返回值类型与该常量相同的函数。例如:stonetolb()返回一个int值,这意味着可以这样:
int aunt = stonetolb(20);
int aunts = aunt + stonetolb(10);
cout << "Perdie weights " << stonetolb(16) << " pounds." << endl;
这些例子表明,函数原型描述了函数接口,即函数如何与程序的其他部分交互。参数列表指出了何种信息将被传递给函数,函数类型指出了返回值的类型。
在多函数程序中使用using编译指令
using namespace std;
这是因为每个函数都使用了cout,因此需要能够访问位于名称空间std中的cout定义。
让程序能够访问名称空间std的方法有很多种,下面是其中的4种:
-
将using namespace std; 放在函数定义之前,让文件中所有的函数都能够使用名称空间std中所有的元素。 -
将using namespace std; 放在特定的函数定义中,让该函数能够使用名称空间std中的所有元素。 -
在特定的函数中使用类似using std::cout; 这样的编译指令,而不是using namespace std; ,让该函数能够指定的元素,如cout。 -
完全不使用编译指定using,而在需要使用名称空间std中的元素时,使用前缀std:: ,如下: std::cout << "I'm using cout and endl from the std namespace" << std::endl;
第三章 处理数据
面向对象编程(OOP)的本质时设计并扩展自己的数据类型。设计自己的数据类型就是让自己类型与数据匹配。
3.1 简单变量
变量名:
C++提倡使用有一定含义的变量名,如果变量表示差旅费,应将其命名为cost_of_trip 或者 costOfTrip,而不要将其命名为x或cot。必须遵循几种简单的C++命名规则。
- 在名称中只能使用字母字符、数字和下划线。
- 名称的第一个字符不能是数字。
- 区分大写字符与小写字符。
- 不能将C++关键字用作名称。
- 以两个下划线或下划线和大写字母打头的名称被保留给实现(编译器及其使用的资源)使用。以一个下划线开头的名称被保留给实现,用作全局标识符。
- C++对于名称的长度没有限制,名称中所有的字符都有意义,但有些平台有长度限制。
倒数第二点与前面几点有些不同,因为使用像_time_stop 或 _Donut这样的名称不会导致编译器错误,而会导致行为的不确定性。换句话说,不知道结果将会是什么。不出现编译器错误的原因是:这样的名称不是非法的,但是留给实现使用。全局名称指的是名称被声明的位置。
如果想用两个或者更多的单词组成一个名称,通常的做法是用下划线将单词分开,如 my_oninons;或者从第二个单词开始将每个单词的第一个字母大写,如 myEyeTooth。
整型:
不同C++整型使用不同的内存量来存储整数。使用的内存量越大,可以表示的整数值范围也越大,另外,有的类型(符号类型)可表示正值和负值,而有的类型(无符号类型)不能表示负值。
C++的基本整型(按宽度递增的顺序排列)分别是char、short、int、long和C++11新增的long long ,其中其中每种类型都有符号版本和无符号版本因此总共有10种类型可供选择。
short score; //creates a type short integer variable
int temperature; //creates a type int integer variable
long position; //creates a type long integer variable
想知道系统中整数的最大长度,可以在程序中使用C++ 工具来检查类型的长度。
sizeof 运算符返回类型或变量的长度,单位字节(运算符是内置的语言元素,对一个或多个数据进行运算,并生成一个值。例如,加号运算符【+】将两个值相加)。
#include <iostream>
#include <climits> //use limits.h for older systems
using namespace std;
int main()
{
int n_int = INT_MAX; //initialize n_int max int value
short n_short = SHRT_MAX; //symbols defined in climits file
long n_long = LONG_MAX;
long long n_llong = LLONG_MAX;
//sizeof operator yields size of type or o variable
cout << "int is " << sizeof(int) << " bytes." << endl;
cout << "short is " << sizeof n_short << " bytes." << endl;
cout << "long is " << sizeof n_long << " bytes." << endl;
cout << "long long is " << sizeof n_llong << " bytes." << endl;
cout << endl;
cout << "Maximum values:" << endl;
cout << "int: " << n_int << endl;
cout << "short: " << n_short << endl;
cout << "long: " << n_long << endl;
cout << "long long :" << n_llong << endl;
cout << "Minimum int value = " << INT_MIN << endl<< endl;
cout << "BITS per byte = " << CHAR_BIT << endl;
return 0;
}
运行结果:
int is 4 bytes.
short is 2 bytes.
long is 4 bytes.
long long is 8 bytes.
Maximum values:
int: 2147483647
short: 32767
long: 2147483647
long long :9223372036854775807
Minimum int value = -2147483648
BITS per byte = 8
注意:如果您的系统不支持类型long long,应删除使用该类型的代码行。
运算符sizeof 和 头文件 limits
可对类型名或变量名使用sizeof运算符。对类型名(如 int)使用sizeof 运算符时,应将名称放在括号中;但对变量名(如 n_short)使用该运算符,括号时可选的:
cout << "int is" << sizeof(int) << "bytes.\n";
cout << "short is" << sizeof n_short << "bytes.\n";
climits文件中包含与下面类似的语句行:
#define INT_MAX 32767
在C++编译过程中,首先将源代码传递给预处理器。在这里,#define和#include 一样,也是一个预处理器编译指令。该编译指令告诉预处理器:在程序中查找 INT_MAX,并将所有的INT_MAX 都替换为32767。因此#define 编译指令的工作方式与文本编辑器或字处理器中的全局搜索并替换命令相似。
**#define 编译指令是 C 语言留下来的,C++有一种更好的创建符号常量的方法(使用关键字const ),所以不会经常使用#define。**然而,有些头文件,尤其是那些被设计成可用于C和C++中的头文件,必须使用#define。
初始化
初始化将赋值与声明合并在一起。例如,下面的语句声明了变量n_int,并将int的最大取值赋给它:
int n_int = INT_MAX;
也可以使用字面值常量来初始化。可以将变量初始化为另一个变量,条件是后者已经定义过。
甚至可以使用表达式来初始化变量,条件是当程序执行到该声明时,表达式中所有的值都是已知的:
int uncles = 5; //initialize uncles to 5
int aunts = uncles; //initialize aunts to 5
int chairs = aunts + uncles + 4; //initialize chairs to 14
C++11 初始化方式
有一种初始化方式用于数组和结构,但在C++98 中,也可以用于单值变量:
int hamburgers = {24}; // set hamburgers to 24
将大括号初始化器用于单值变量的情形还不多,但在C++11 标准使得这种情形更多了。首先,采用这种方式时,可以使用等号(=),也可以不使用。
int emus{7}; //set emus to 7
int rheas = {12}; //set rheas to 12
其次,大括号内可以不包含任何东西。在这种情况下,变量将初始化为零。
int rocs = {}; // set rocs to 0
int psychics{}; //set psychics to 0
无符号类型
整型都有一种不能存储负数值的无符号变体,其优点是可以增大变量能够存储的最大值。
例如,如果short表示的范围为-32768 到 +32767 ,则无符号版本的表示范围为 0 ~ 65535.当然,仅当数值不会为负时才应使用无符号类型。
要创建无符号版本的基本整型,只需使用关键字unsigned 来修改声明即可:
unsigned short change; //unsigned short type
unsigned int rovert; //unsigned int type
unsigned quarterback; //also unsigned int
unsigned long gone; //unsigned long type
unsigned long long lang_lang; //unsigned long long type
注意:unsigned 本身是 unsigned int 的缩写。
看一看预处理器语句 #define
#include <iostream>
#define ZERO 0 //makes ZERO symbol for 0 value
#include <climits> //defines INT_MAX as largest int value
using namespace std;
int main()
{
short sam = SHRT_MAX; //initialize a variable to max value
unsigned short sue = sam; //okay if variable sam already defined
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited." << endl
<< "Add $1 to each account." << endl << "Now ";
sam = sam + 1;
sue = sue + 1;
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited.\nPoor Sam!" << endl;
sam = ZERO;
sue = ZERO;
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited." << endl;
cout << "Take $1 from each account." << endl << "Now ";
sam = sam -1;
sue = sue -1;
cout << "Sam has " << sam << " dollars and Sue has " << sue;
cout << " dollars deposited." << endl << "Lucky Sue!" << endl;
return 0;
}
运行结果:
Sam has 32767 dollars and Sue has 32767 dollars deposited.
Add $1 to each account.
Now Sam has -32768 dollars and Sue has 32768 dollars deposited.
Poor Sam!
Sam has 0 dollars and Sue has 0 dollars deposited.
Take $1 from each account.
Now Sam has -1 dollars and Sue has 65535 dollars deposited.
Lucky Sue!
整型字面值
#include <iostream>
using namespace std;
int main()
{
int chest = 42; // decimal integer literal
int waist = 0x42; // hexadecimal integer literal
int inseam = 042; // octal integer literal
cout << "Monsieur cuts a striking figure!\n";
cout << "chest = " << chest << "(42 in decimal)\n";
cout << "waist = " << waist << "(0x42 in hex)\n";
cout << "inseam = " << inseam << "(042 in octal)\n";
return 0;
}
运行结果:
Monsieur cuts a striking figure!
chest = 42(42 in decimal)
waist = 66(0x42 in hex)
inseam = 34(042 in octal)
控制符dec、hex和oct,分别用于指示cout以十进制、十六进制和八进制格式显示整数。
#include <iostream>
using namespace std;
int main()
{
int chest = 42;
int waist = 42;
int inseam = 42;
cout << "Monsieur cuts a striking figure!" << endl;
cout << "chest = " << chest << "(decimal for 42)" << endl;
cout << hex; //manipulator for changing number base
cout << "waist = " << waist << "(hexadecimal for 42)" << endl;
cout << oct; //manipulator for changing number base
cout << "inseam = " << inseam << "(octal for 42)" << endl;
return 0;
}
运行结果:
Monsieur cuts a striking figure!
chest = 42(decimal for 42)
waist = 2a(hexadecimal for 42)
inseam = 52(octal for 42)
诸如cout << hex; 等代码不会在屏幕上显示任何内容,而只是修改cout显示整数的方式。
因此,控制符hex实际上是一条消息,告诉cout采取何种行为。
C++如何确定常量的类型
程序的声明将特定的整数变量的类型告诉了C++编译器,但编译器是如何知道常量的类型呢?假设在程序中使用常量表示一个数字:
cout << "Year = " << 1492 << "\n";
程序将把1492存储为int、long还是其他整型呢?答案是,除非有理由存储为其他类型(如使用了特殊的后缀来表示特定的类型,或者值太大,不能存储int),否则C++将整型常量存储为int类型。
char 类型
#include <iostream>
using namespace std;
int main()
{
char ch; //declare a char variable
cout << "Enter a character: " << endl;
cin >> ch;
cout << "Hola!";
cout << "Thank you for the " << ch << " character." << endl;
return 0;
}
运行结果:
Enter a character:
M
Hola!Thank you for the M character.
有趣的是,程序中输入的是M,而不是对应的字符编码77。另外,程序将打印M,而不是77.
通过查看内存可知,77是存储在变量ch中的值,这种神奇的力量不是来自char类型,而是来自cin和cout,这些工具为您完成了转换工作。
输入时,cin将键盘输入的M转换为77;输出时,cout将值77转换为所显示的字符M;cin和cout的行为都是由变量类型引导的。
cout.put()函数
程序引入了cout的一项特性:cout.put()函数,该函数显示一个字符。
#include <iostream>
using namespace std;
int main()
{
char ch = 'M'; //assign ASCII code for M to ch
int i = ch; //store same code in and int
cout << "The ASCII code for " << ch << " is " << i << endl;
cout << "Add one to the character code:" << endl;
ch = ch + 1; //change character code in ch
i = ch; //save new character code in i
cout << "The ASCII code for " << ch << " is " << i << endl;
//using the cout.put() member function to dispaly a char
cout << "Displaying char ch using cout.put(ch):";
cout.put(ch);
//using cout.put() to display a char constant
cout.put('!');
cout << endl << "Done" << endl;
return 0;
}
运行结果:
The ASCII code for M is 77
Add one to the character code:
The ASCII code for N is 78
Displaying char ch using cout.put(ch):N!
Done
C++将字符表示为整数提供了方便,使得操纵字符值很容易。不必使用笨重的转换函数在字符和ASCII码之间来回转换。
即使通过键盘输入的数字也将被视为字符。
char ch;
cin >> ch;
//如果您输入5并按回车键,则读取字符“5”,并将其对应的字符编码(ASCII 编码 53)存储到变量ch中。
int n;
cin >> n;
//如果您输入5并按回车键,则读取字符“5”,并将其转换为相应的数字值5,并存储到变量n中。
char字面值
在C++中,书写字符常量的方式有多种,对于常规字符(如字母、标点符号和数字),最简单的方法是将字符用单引号括起。这种表示法代表的是字符的数值编码。
例如:ASCII系统中的对应情况如下:
- 'A’为65,即字符A的ASCII码
- 'a’为97,即字符a的ASCII码
- '5’为53,即数字5 的ASCII码
- ''为32,即空格字符的ASCII码
- '!'为33,即惊叹号的ASCII码
有些字符不能直接通过键盘输入到程序中。例如,按回车键不能使字符包含换行符。
C++提供了一种特殊的表示方法:转义序列。
转义序列\n表示换行符。\a表示振铃字符。
char alarm = '\a';
cout << alarm << "Don't do that again!\a\n";
cout << "Ben \"Buggsie\" Hacker \n was here!\n";
//最后一行输出如下:
Ben "Buggsie" Hacker
was here!
C++转义序列的编码
字符名称 | ASCII符号 | C++代码 | 十进制ASCII码 | 十六进制ASCII码 |
---|
换行符 | NL(LF) | \n | 10 | 0xA | 水平制表符 | HT | \t | 9 | 0x9 | 垂直制表符 | VT | \v | 11 | 0xB | 退格 | BS | \b | 8 | 0x8 | 回车 | CR | \r | 13 | 0xD | 振铃 | BEL | \a | 7 | 0x7 | 反斜杠 | \ | \\ | 92 | 0x5C | 问号 | ? | ? | 63 | 0x3F | 单引号 | ’ | \ ’ | 39 | 0x27 | 双引号 | " | \ " | 34 | 0x22 |
注意:应该像处理常规字符(如Q)那样处理转义序列(如\n)。也就是说,将它们作为字符常量时,应用单引号括起;将它们放在字符串中时,不要使用单引号。
换行符可替代endl,用于在输出中重起一行。可以义字符常量表示法(’\n’)或字符串方式(“n”)使用换行符。
cout << endl; //using the endl manipulator
cout << '\n'; //using a character constant
cout << "\n"; //using a string
可以将换行符嵌入到较长的字符串中,这通常比使用endl方便。例如:
cout << endl << endl << "What next?" << endl << "Enter a number:" << endl;
cout << "\n\n What next? \n Enter a number: \n";
显示数字时,使用endl比输入"\n"或’\n’更容易点,但显示字符串时,在字符串末尾添加一个换行符所需的输入量要少一些:
cout << x << endl; //easier than cout << X << "\n";
cout << "Dr. X. \n"; //easier than cout << "The Dr.X." << endl;
Houdini曾经在只使用转义序列的情况下,绘制了一副哈得逊河图。
#include <iostream>
using namespace std;
int main()
{
cout << "\aOperation \"HyperHype\" is now activated!\n";
cout << "Enter your agent code:_________\b\b\b\b\b\b\b\b";
long code;
cin >> code;
cout << "\aYou entered " << code << "...\n";
cout << "\aCode verified! Proceed with Plan Z3!\n";
return 0;
}
signed char 和 unsigned char
与int 不同的是,char 在默认情况下既不是没有符号,也不是有符号,是否有符号由C++实现决定,这样编译器开发人员可以最大限度地将这种类型与硬件属性匹配起来。
如果char有某种特定的行为对您来说非常重要,则可以显示地将类型设置为signed char或 unsigned char:
char fodo; //may be signed, may be unsigned
unsigned char bar; //definitely unsigned
signed char snark; //definitely signed
unsigned char类型的表示范围通常为0 ~ 255,而signed char 的表示范围为 -128 ~ 127。
bool类型
ANSI/ISO C++标准添加了一种名叫bool的新类型(对C++来说是新的)。在计算中,布尔变量的值可以是true或false。
C++将非零值解释为true,将零解释为false。
可以使用bool类型来表示真(true)和假(false):
bool is_ready = true;
字面值true 和false 都可以通过提升转换为int类型,true被转换为1,而false被转换为0;
int ans = true; // ans assigned 1
int promise = false; // promise assigned 0
另外,任何数字值或指针值都可以被隐式转换(即不用显示强制转换)为bool值。任何非零值都被转换为true,而零被转换为false。
bool start = -100; // start assigned true
bool stop = 0; // stop assigned false
3.2 const 限定符
常量的符号名称指出了常量表示的内容。另外,如果程序在多个地方使用同一个常量,则需要修改该常量时,只需修改一个符号定义即可。
前面有介绍过#define语句的说明,C++有一种更好的处理符号常量的方法,这种方法就是使用const关键字来修改变量声明和初始化。
例如:假设需要一个表示一年中月份数的符号常量:
const int Months = 12; //MOnths is symbolic constant for 12
常量被初始化后,其值就被固定了,编译器将不允许再修改该常量的值。
关键字const叫做限定符,因为它限定了声明的含义。
一种常见的做法是将名称的首字母大写,已提醒您Months是个常量,有助于区分常量和变量。另一种约定是将整个名称大写,使用#define 创建常量时通常使用这种约定。
创建常量的通用格式如下:
const type name = value;
注意:应在声明中对const进行初始化。下面的代码不好:
const int toes; //value of toes undefined at this point
toes = 10; // tool late!
如果在声明常量时没有提供值,则该常量的值将是不确定的,且无法修改。
提示:如果打算使用#define来定义符号常量,请不要这样做,而应使用const。
3.3 浮点数
#include <iostream>
using namespace std;
int main()
{
cout.setf(ios_base::fixed,ios_base::floatfield); //fixed-point
float tub = 10.0 / 3.0; //good to about 6 places
double mint = 10.0 / 3.0; // good to about 15 places
const float million = 1.0e6;
cout << "tub = " << tub;
cout << ", a million tubs = " << million * tub;
cout << ",\n and ten million tubs = ";
cout << 10 * million * tub << endl;
cout << "mint = " << mint << " and a million mints = ";
cout << million * mint << endl;
return 0;
}
运行结果:
tub = 3.333333, a million tubs = 3333333.250000,
and ten million tubs = 33333332.000000
mint = 3.333333 and a million mints = 3333333.333333
浮点常量
==在默认情况下,浮点常量都属于double类型。==如果希望常量为float类型,请使用f或者F后缀。
对于long double类型,可使用l或者L后缀(由于l看起来像数字1,因此L是更好的选择)。
1.234f //a float constant
2.45E20F // a float constant
2.345324E28 //a double constant
2.2L // a long double constant
浮点数的优缺点
与整数相比,浮点数有两大点。首先,它们可以表示整数之间的值;其次,由于有缩放因子,它们可以表示的范围大得多。另一方面,浮点运算的速度通常比整数运算慢,且精度将降低。
#include <iostream>
using namespace std;
int main()
{
float a = 2.34E+22f;
float b = a + 1.0f;
cout << "a = " << a << endl;
cout << "b - a = " << b-a << endl;
return 0;
}
注意:有些基于ANSI C 之前的编译器的老式C++实现不支持浮点数常量后缀f。如果出现这样的问题,可用2.34E+22代替 2.34E+22f,用(float)1.0 代替 1.0f。
3.4 C++算术运算符
除法分支
#include <iostream>
using namespace std;
int main()
{
cout.setf(ios_base::fixed,ios_base::floatfield);
cout << "Integer division: 9/5 = " << 9 / 5 << endl;
cout << "Floating-point division: 9.0/5.0 = ";
cout << 9.0 / 5.0 << endl;
cout << "Mixed division: 9.0 / 5 = " << 9.0 / 5 << endl;
cout << "double constants: 1e7/9.0 = ";
cout << 1.e7 / 9.0 << endl;
cout << "float constants: 1e7f / 9.0f = ";
cout << 1.e7f / 9.0f << endl;
return 0;
}
运行结果:
Integer division: 9/5 = 1
Floating-point division: 9.0/5.0 = 1.800000
Mixed division: 9.0 / 5 = 1.800000
double constants: 1e7/9.0 = 1111111.111111
float constants: 1e7f / 9.0f = 1111111.125000
可以用(float)1.e7 / (float)9.0 代替 1.e7f / 9.0f。
实际上,对不同类型进行运算时,C++将把它们全部转换为同一类型。
如果两个操作符都是double类型,则结果为double类型;如果两个操作符都是float类型,则结果为float类型。记住,浮点常量在默认情况下为double类型。
强制类型转换
#include <iostream>
using namespace std;
int main()
{
int auks,bats,coots;
//the following statement adds the value as double
//then coverts the result to int
auks = 19.9 + 11.9;
//these statements add values as int
bats = (int)19.99 + (int)11.99; // old C syntax
coots = int (19.99) + int (11.99); // new C++ syntax
cout << "auks = " << auks << ", bats = " << bats;
cout << ",coots = " << coots << endl;
char ch = 'Z';
cout << "The code for " << ch << " is "; //print as char
cout << int(ch) << endl; //print as int
cout << "Yes,the code is ";
cout << static_cast<int>(ch) << endl; //using static_cast
return 0;
}
运行结果:
auks = 31, bats = 30,coots = 30
The code for Z is 90
Yes,the code is 90
C++11 中的auto声明
C++11新增了一个工具,让编译器能够根据初始值的类型推断变量的类型。为此,它重新定义了auto的含义。auto时一个C语言关键字,但很少使用。
如果使用关键字auto,而不指明变量的类型,编译器将把变量的类型设置成与初始值相同。
auto n = 100; // n is int
auto x = 1.5; // x is double
auto y = 1.3e12L; // y is long double
总结
C++的基本类型分为两组:一组由存储为整数的值组成,另一组由存储为浮点格式的值组成。整型之间通过存储值时使用的内存量及有无符号来区分。
整型从最小到最大依次是:bool、char、signed char、unsigned char 、short 、unsigned short 、int 、unsigned int、long 、unsigned long 以及C++新增的long long 和 unsigned long long。
第四章 复合类型
4.1 数组
数组(array)是一种数据格式,能够存储多个同类型的值。
例如,数组可以存储60个int类型的值、12个short值(这些值表示每个月的天数)或365个float值(这些值指出一年中每天在食物方面的开销)。每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素。
要创建数组,可使用声明语句。数组声明应指出以下三点:
- 存储在每个元素中的值的类型;
- 数组名;
- 数组中的元素数。
在C++中,可以通过修改简单变量的声明,添加中括号(其中包括元素数目)来完成数组声明。例如:声明创建一个名为months的数组,该数组有12个元素,每个元素都可以存储一个short类型的值:
short months[12]; //create array of 12 short
事实上,可将数组中的每个元素看作是一个简单变量。
数组声明的通用格式如下:
typeName arrayName[arraySize];
表达式arraySize 指定元素数组,它必须是整型常数(如10)或 const值,也可以是常量表达式(8 * sizeof(int)),即其中所有的值在编译时都是 已知的。具体来说,arraySize不能是变量,变量的值是在程序运行时设置的。然而,可以使用new运算符来避开这种限制。
数组之所以被称为复合类型,是因为它是使用其他类型来创建的。不能仅仅将某种东西声明为数组,它必须是特定类型的数组。没有通用的数组类型,但存在很多特定的数组类型,如char数组或者long数组。
float loans[20];
loans 的类型不是“数组”,而是“float数组”。这强调了loans数组是使用float类型创建的。
数组的很多用途都是基于这样的一个事实:可以单独访问数组元素。方法是使用下标或索引来对元素进行编号。C++数组从0开始编号。C++使用带索引的方括号表示法来指定数组元素。
注意:最后一个元素的索引比数组长度小1。因此,数组声明能够使用一个声明创建大量的变量,然后便可以用索引来标识和访问各个元素。
#include <iostream>
using namespace std;
int main(){
int yams[3]; // creates array with three elements
yams[0] = 7; // assign value to first element
yams[1] = 8;
yams[2] = 6;
int yamcosts[3] = {20,30,5}; //create,initialize array
cout << "Total yams = " ;
cout << yams[0] + yams[1] + yams[2] << endl;
cout << "The package with " << yams[1] << " yams costs";
cout << yamcosts[1] << " cents per yam.\n";
int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1];
total = total + yams[2] * yamcosts[2];
cout << "The total yam expense is " << total << " cents.\n";
cout << "\nSize of yams array = " << sizeof yams;
cout << " bytes.\n";
cout << "Size of one element = " << sizeof yams[0];
cout << " bytes.\n";
return 0;
}
运行结果:
Total yams = 21
The package with 8 yams costs30 cents per yam.
The total yam expense is 410 cents.
Size of yams array = 12 bytes.
Size of one element = 4 bytes.
数组的初始化
只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组。
int cards[4] = {3,6,8,10}; // okay
int hand[4]; // okay
hand[4] = {5,6,7,9}; // not allowed
hand = cards; // not allowed
然而,可以使用下标分别给数组中的元素赋值。
初始化数组时,提供的值可以少于数组的元素数目。例如:
float hotelTips[5] = {5.0,2.5};
如果只对数组的一部分进行初始化,则编译器将把其他元素设置为0.因此,将数组中所有的元素都初始化为0非常简单:只要显式地将第一个元素初始化为0,然后让编译器将其他元素都初始化为0即可。
long totals[500] = {0};
如果初始化为{1}而不是{0},则第一个元素被设置为1,其他元素都被设置为0。
如果初始化数组时方括号内([ ])为空,C++编译器将计算元素个数。例如:
short things[] = {1,5,3,8};
编译器将使things数组包含4个元素。
C++11 数组初始化方法
C++11中的列表初始化新增了一些功能。
首先,初始化数组时,可省略等号(=);
double earnings[4] {1.2e4,1.6e4,1.1e4,1.7e4}; //okay with c++11
其次,可不在大括号内包含任何东西,这将把所有元素都设置为零:
unsigned int counts[10] = {}; // all elements set to 0
float balances[100] {}; // all elements set to 0
第三,列表初始化禁止缩窄转换:
long plifs[] = {25,92,3.0}; // not allowed
char slifs[4] {'h','i',1122011,'\0'}; //not allowed
char tlifs[4] {'h','i',112,'\0'}; // allowed
第一条语句不能通过编译,因为将浮点数转换为整型是缩窄操作,即使浮点数的小数点后面为零。
第二条语句也不能通过编译,因为1122011超出了char变量的取值范围。
第三条语句可通过编译,因为虽然112是一个int值,但它在char变量的取值范围内。
C++标准模板库(STL)提供了一种数组替代品:模板类vector,而C++11新增了模板类array。
4.2 字符串
字符串是存储在内存的连续字节中的一系列字符。C++处理字符串的方式有两种。第一种来自C语言,常被称为C-风格字符串(C-style string)。另一种是基于string类库的方法。
存储在连续字节中的一系列字符意味着可以将字符串存储在char数组中,其中每个字符都位于自己的数组元素中。字符串提供了一种存储文本信息的便捷方式,如提供给用户的消息(“请告诉我您的瑞士银行账号”)或来自用户的响应(“您肯定在开玩笑”)。
C-风格字符串具有一种特殊的性质:以空字符(null character)结尾,空字符被写作\0,其中ASCII码为0,用来标记字符串的结尾。
char dog[8] = {'b','e','a','u','','I','I'}; // not a string
char cat[8] = {'f','a','t','e','s','s','a','\0'}; // a string
这两个数组都是char数组,但只有第二个数组是字符串。
在cat数组中使用了大量的单引号,且必须加上空字符。但有一种更好的将字符数组初始化为字符串的方法:只需使用一个用引号括起的字符串即可,这种字符串被称为字符串常量(string constant)或是字符串字面值(string literal)。
char bird[11] = "Mr. Cheeps"; // the \0 is understood
char fish[] = "Bubbles"; // let the compier count
**用引号括起的字符串隐式地包括结尾的空字符,因此不用显示地包括它。**另外,各种C++输入工具通过键盘输入,将字符串读入到char数组中时,将自动加上结尾的空字符。
让数组比字符串长没有什么害处,只是会浪费一些空间而已。这是因为处理字符串的函数根据空字符的位置,而不是数组长度来进行处理。
C++对字符串长度没有限制。
警告:在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内。
注意:字符串常量(使用双引号)不能与字符常量(使用单引号)互换。字符常量(如’S’)是字符串编码的简写表示。
char shirt_size = 'S'; // this is fine
但"S"不是字符常量,它表示的是两个字符(字符S和\0)组成的字符串。"S"实际上表示的是字符串所在的内存地址。
char shirt_size = "S"; //illegal type mismatch
由于地址在C++中是一种独立的类型,因此C++编译器不允许这种不合理的做法。
拼接字符串常量
有时候,字符串很长,无法放到一行中。C++允许拼接字符串字面值,即将两个用引号括起来的字符串合并为一个。事实上,任何两个由空白(空格、制表符和换行符)分割的字符串常量都将自动拼接成一个。因此,下面所有的输出语句都是等效的:
cout << "I'd give my right arm to be" " a great violinist.\n";
cout << "I'd give my right arm to be a great violinist.\n";
cout << "I'd give my right ar"
"m to be a great violinist.\n";
注意:拼接时不会在被连接的字符串之间添加空格,第二个字符串的第一个字符串将紧跟在第一个字符串的最后一个字符(不考虑\0)后面。第一个字符串中的\0字符将被第二个字符串的第一个字符取代。
在数组中使用字符串
要将字符串存储到数组中,最常有的方法有两种:将数组初始化为字符串常量、将键盘或文件输入读入到数组中。
下面程序演示了这两种方法,它将一个数组初始化为用引号括起来的字符串,并将使用cin将一个输入字符串放到另一个数组中。该程序还使用了标准C语言库函数strlen()来确定字符串的长度。标准头文件cstring(老式实现为string.h)提供了该函数以及很多与字符串相关的其他函数的声明。
#include <iostream>
#include <cstring> // for the strlen() function
using namespace std;
int main(){
const int Size = 15;
char name1[Size]; // empty array
char name2[Size] = "C++owboy"; // initialized array
cout << "Howdy! I'm " << name2;
cout << "! What's your name?\n";
cin >> name1;
cout << "Well, " << name1 << ",your name has ";
cout << strlen(name1) << " letters and is stored\n";
cout << "in an array of " << sizeof(name1) << "bytes.\n";
cout << "Your initial is " << name1[0] << ".\n";
name2[3] = '\0'; // 使用 \0 截短字符串
cout << "Here are the first 3 characters of my name: ";
cout << name2 << endl;
return 0;
}
运行结果:
Howdy! I'm C++owboy! What's your name?
Basicman
Well, Basicman,your name has 8 letters and is stored
in an array of 15bytes.
Your initial is B.
Here are the first 3 characters of my name: C++
sizeof运算符指出整个数组的长度:15字节,但是strlen()函数返回的是存储在数组中的字符串的长度。
字符串的输入
#include <iostream>
using namespace std;
int main(){
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name:\n";
cin >> name;
cout << "Enter your favorite dessert:\n";
cin >> dessert;
cout << "I have some dellicious " << dessert;
cout << " for you, " << name << ".\n";
return 0;
}
运行结果:
Enter your name:
Alistair Dreeb
Enter your favorite dessert:
I have some dellicious Dreeb for you, Alistair.
我们甚至还没有对"输入甜点的提示" 作出反应,程序便把它显示出来了,然后立即显示最后一行。cin使用空白(空格、制表符和换行符)来确定字符串的结束位置。这意味着cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。
这个例子的实际结果是,cin把Alistair作为第一个字符串,并将它放到了name数组中。这把Dreeb留在输入队列中。当cin在输入队列中搜素用户喜欢的甜点时,它发现了Dreeb,因此cin读取Dreeb,并将其放到dessert数组中。
每次读取一行字符串输入
每次读取一个单词通常不是最好的选择。例如,假设程序要求用户输入城市名,用户输入New York或Sao Paulo。您希望程序读取并存储完整的城市名,而不仅仅是New或Sao。要将整条短语而不是一个单词作为字符串输入,需要采用另一种字符串读取方法。具体地说,需要采用面向行而不是面向单词的方法。幸运的是,istream中的类(如cin)提供了一些面向行的类成员函数:getline()和get()。这两个函数都读取一行输入,直到到达换行符。然而,随后getline()将丢弃换行符,而get()将换行符保留在输入序列中。
面向行的输入:getline()
getline()函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。通常要调用这种方法,可以使用cin.getline()。该函数有两个参数。第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。如果这个参数为20,则函数最多读取19个字符。余下的空间用于存储自动在结尾处添加的空字符。getline()成员函数在读取指定数目的字符或遇到换行符时停止读取。
例如,假设要使用getline()将姓名读入到一个包含20个元素的name数组中。可以这样调用函数:
cin.getline(name,20);
这将把一行读入到name数组中,如果这行包含的字符不超过19个。
#include <iostream>
using namespace std;
int main(){
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name:\n";
cin.getline(name,ArSize); //reads through newline
cout << "Enter your favorite dessert:\n";
cin.getline(dessert,ArSize);
cout << "I have some dellicious " << dessert;
cout << " for you, " << name << ".\n";
return 0;
}
运行结果:
Enter your name:
Dirk Hammernose
Enter your favorite dessert:
Radish Torte
I have some dellicious Radish Torte for you, Dirk Hammernose.
getline()函数每次读取一行,它通过换行符来确定行尾,但不保存换行符。相反,在存储字符串时,它用空字符来替换换行符。
面向行的输入:get()
istream 类有另一个名为 get() 的成员函数,该函数有几种变体。其中一种变体的工作方式与getline()类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但get并不再读取并丢弃换行符,而是将其留在输入队列中。
cin.get(name,ArSize); // read first line
cin.get(); // read newline
cin.get(dessert,ArSize); // read second line
另一种使用get()的方式是将两个类成员函数拼接起来(合并)。
cin.get(name,ArSize).get(); // concatenate member functions
之所以可以这样做,是由于cin.get(name,ArSize) 返回一个cin对象,该对象随后将被用来调用get()函数。同样,下面的语句将把输入中的连续的两行分别读入到数组name1 和 name2 中,其效果与两次调用cin.getline()相同。
cin.getline(name1,ArSize).getline(name2,ArSize);
程序清单:
#include <iostream>
using namespace std;
int main(){
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name:\n";
cin.get(name,ArSize).get(); //read string, newline
cout << "Enter your favorite dessert:\n";
cin.get(dessert,ArSize).get();
cout << "I have some dellicious " << dessert;
cout << " for you, " << name << ".\n";
return 0;
}
运行结果:
Enter your name:
Mai Parfait
Enter your favorite dessert:
Chocolate Mousse
I have some dellicious Chocolate Mousse for you, Mai Parfait.
为什么要使用get(),而不是getline()呢?首先,老式实现没有getline(),其次,get()使输入更仔细。例如:假设用get()将一行读入数组中。如何知道停止读取的原因是由于已经读取了整行,而不是由于数组已经填满呢?查看下一个输入字符,如果是换行符,说明已经读取了整行;否则,说明该行中还有其他的输入。
总之,getline()使用起来简单一些,但是get()使得检查错误更简单些。可以用其中的任何一个来读取一行输入。
空行和其他问题
当getline()或get()读取空行时,将发生什么情况?最初的做法是,下一条输入语句将在前一条getline()或get()结束读取的位置开始读取。但当前的做法是,当get()读取空行后将设置失效位。这意味着接下来的输入将被阻断,但可以使用cin.clear(); 来恢复输入。
另一个潜在的问题是,输入字符串可能比分配的空间长,如果输入行包含的字符数比指定的多,则getline()和get()将把余下的字符留在输入队列中,getline()还会设置失效位,并关闭后面的输入。
混合输入字符串和数字
#include <iostream>
using namespace std;
int main(){
cout << "What year was your house built?\n";
int year;
cin >> year;
cout << "What is its street address?\n";
char address[80];
cin.getline(address,80);
cout << "Year built: " << year << endl;
cout << "Address: " << address << endl;
cout << "Done!\n";
return 0;
}
运行结果:
What year was your house built?
1966
What is its street address?
Year built: 1966
Address:
Done!
用户根本没有输入地址的机会。问题在于,当cin读取年份,将回车键生成的换行符留在了输入队列中。后面的cin.getline()看到换行符后,将认为是一个空行,并将一个字符串赋给address数组。
解决方法:在读取地址之前先读取并丢弃换行符。
cin >> year;
cin.get(); // or cin.get(ch);
或者利用表达式cin>>year返回cin对象,将调用拼接起来:
(cin >> year).get(); // or (cin >> year).get(ch);
C++程序常使用指针(而不是数组)来处理字符串。
4.3 string 类
ISO/ANSI C++98 标准通过添加string 类扩展了C++库,因此现在可以string类型的变量(使用C++的话说是对象)而不是字符数组来存储字符串。
string 类使用起来比数组简单,同时提供了将字符串作为一种数据类型的表示方法。
要使用string 类,必须在程序中包含头文件string。string 类位于名称空间std中,因此必须提供一条using 编译指令,或者使用 std::string 来引用它。
sting 类定义隐藏了字符串的数组性质,能够像处理普通变量那样处理字符串。
#include <iostream>
#include <string> // make string class available
using namespace std;
int main(){
char charr1[20]; // create an empty array
char charr2[20] = "jaguar"; // create an initialized array
string str1; // create an empty string object
string str2 = "panther"; // create an initialized string
cout << "Enter a kind of feline: ";
cin >> charr1;
cout << "Enter another kind of feline: ";
cin >> str1;
cout << "Here are some felines:\n";
cout << charr1 << " " << charr2 << " "
<< str1 << " " << str2 << endl; // use cout for output
cout << " The third letter in " << charr2 << " is "
<< charr2[2] << endl;
cout << " The third letter in " << str2 << " is "
<< str2[2] << endl; //use array notation
return 0;
}
运行结果:
Enter a kind of feline: ocelot
Enter another kind of feline: tiger
Here are some felines:
ocelot jaguar tiger panther
The third letter in jaguar is g
The third letter in panther is n
string 对象 和 字符数组之间的主要区别是:
可以将 string 对象声明为简单变量,而不是数组。
string str1; // create an empty string object
string str2 = "panther"; // create an initialized string
类设计让程序能够自动处理string 的大小。例如:
str1 的声明创建一个长度为0的string 对象,但程序将输入读取到str1 中时,将自动调整str1 的长度。
cin >> str1 ; // str1 resized to fit input
这使得与数组相比,使用string 对象更方便,也更安全。
从理论上,可以将char数组视为一组用于存储一个字符串的char存储单元,而string类变量是一个表示字符串的实体。
C++11 字符串初始化
C++11 也允许将列表初始化用于C-风格字符串和string对象:
char first_date[] = {"Le Chapon Dodu"};
char second_date][] {"The Elegant Plate"};
string third_date = {"The Bread Bowl"};
string fourth_date {"Hank's Fine Eats"};
赋值、拼接和附加
使用 string 类时,某些操作比使用数组时更简单。例如,不能将一个数组赋给另一个数组,但可以将一个string对象赋给另一个string对象:
char charr1[20]; // create an empty array
char charr2[20] = "jaguar"; // create an initialized array
string str1; // create an empty string object
string str2 = "panther"; // create an initialized string
charr1 = charr2; // invalid,no array assignment
str1 = str2; // valid,object assignment ok
string 类简化了字符串合并操作。可以使用运算符+将两个string 对象合并起来,还可以使用运算符+=将字符串附加到string 对象的末尾。
string str3;
str3 = str1 + str2; // assign str3 the joined strings
str1 += str2; // add str2 to the end of str1
可以将C-风格字符串或string对象与string 对象相加,或者将它们附加到string 对象的末尾。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1 = "penguin";
string s2,s3;
cout << "You can assign one string object to another: s2 = s1\n";
s2 = s1;
cout << "s1: " << s1 << ",s2: " << s2 << endl;
cout << "You can assign a C-style string to a string object.\n";
cout << "s2 = \"buzzard\"\n";
s2 = "buzzard";
cout << "s2: " << s2 << endl;
cout << "You can concatenate strings: s3 = s1 + s2\n";
s3 = s1 + s2;
cout << "s3: " << s3 << endl;
cout << "You can append strings.\n";
s1 += s2;
cout << "s1 += s2 yields s1 = " << s1 << endl;
s2 += " for a day ";
cout << "s2 += \" for a day\" yields s2 = " << s2 << endl;
return 0;
}
运行结果:
You can assign one string object to another: s2 = s1
s1: penguin,s2: penguin
You can assign a C-style string to a string object.
s2 = "buzzard"
s2: buzzard
You can concatenate strings: s3 = s1 + s2
s3: penguinbuzzard
You can append strings.
s1 += s2 yields s1 = penguinbuzzard
s2 += " for a day" yields s2 = buzzard for a day
string 类的其他操作
在C++新增string类之前,程序员也需要完成诸如给字符串赋值等工作。对于C-风格字符串,程序员使用C语言库中的函数来完成这些任务。头文件 cstring(以前为string.h) 提供了这些函数。
例如:可以使用函数strcpy()将字符串复制到字符数组中,使用函数strcat()将字符串附加到字符组末尾:
strcpy(charr1,charr2); // copy charr2 to charr1
strcat(charr1,charr2); // append contents of charr2 to charr1
程序清单:
#include <iostream>
#include <string> // make string class available
#include <cstring> // C-style string library
using namespace std;
int main(){
char charr1[20];
char charr2[20] = "jaguar";
string str1;
string str2 = "panther";
// assignment for string objects and character arrays
str1 = str2; // copy str2 to str1
strcpy(charr1,charr2); //copy charr2 to charr1
// appending for string objects and character ayyats
str1 += " paste"; //add paste to end of str1
strcat(charr1,"juice"); //add juice to end of charr1
// finding the length of a string object and a C-style string
int len1 = str1.size(); // obtain length of str1
int len2 = strlen(charr1); // obtain length of charr1
cout << "The string " << str1 << " contains "
<< len1 << " characters.\n";
cout << " The string " << charr1 << " contains "
<< len2 << " characters.\n";
return 0;
}
运行结果:
The string panther paste contains 13 characters.
The string jaguarjuice contains 11 characters.
使用C-风格字符串时,需要使用的函数:
strcpy(charr3,charr1); //c语言风格:字符串复制(拷贝)函数
strcat(charr3,charr2); //C语言风格:字符串连接(拼接)
另外,使用字符数组时,总是存在目标数组过小,无法存储指定信息的危险:
char site[10] = "house";
strcat(site," of pancakes"); // memory problem
函数strcat()试图将全部12 个字符复制到数组site中,这将覆盖相邻的内存。这有可能导致程序终止,或者程序继续运行,但数据被损坏。
string 类具有自动调整大小的功能,从而能够避免这种问题的发生。C函数库确实提供了与strcat()和strcpy()类似的函数:strncat()函数和strncpy()函数。它们能接受指出目标数组最大长度的第三个参数,因此更为安全。
两种确定字符串中字符数的方法:
int len1 = str1.size(); // obtain length of str1
int len2 = strlen(charr1); // obtain length of charr1
函数strlen()是一个常规函数,它接受了一个C-风格字符串作为参数,并返回该字符串包含的字符数。函数size()的功能基本上与此相同,但句法不同;str1不是被用作函数参数,而是位于函数名之前,它们之间用句点连接。
这种句法表明,str1 是一个对象,而size()是一个类方法。方法是一个函数,只能通过其所属类的对象进行调用。在这里,str1 是一个string对象,而size()是string类的一个方法。
总之,C函数使用参数来指出要使用哪个字符串,而C++string类对象使用对象名和句点运算符来指出要使用哪个字符串。
string 类I/O
#include <iostream>
#include <string> //C++ style,make string class available
#include <cstring> //C-style string library
using namespace std;
int main(){
char charr[20]; // C-style
string str; // C++ Style
cout << "Length of string in charr before input:"
<< strlen(charr) << endl;
cout << "Length of string in str before input:"
<< str.size() << endl;
cout << "Enter a line of text:\n";
cin.getline(charr,20); // indicate maximum length
cout << "You entered: " << charr << endl;
cout << "Enter another line of text:\n";
getline(cin,str); // cin now an argument; no length specifier
cout << "You entered: " << str << endl;
cout << "Length of string in charr after input: "
<< strlen(charr) << endl;
cout << "Length of string in str after input: "
<< str.size() << endl;
return 0;
}
运行结果:
Length of string in charr before input:1
Length of string in str before input:0
Enter a line of text:
peanut butter
You entered: peanut butter
Enter another line of text:
blueberry jam
You entered: blueberry jam
Length of string in charr after input: 13
Length of string in str after input: 13
在用户输入之前,str 中的 字符串长度为0.这是因为未被初始化的string对象的长度被自动设置为0。
cin.getline(charr,20);
这种句点表示法表明,函数getline()是istream类的一个类的方法(之前所说的,cin是一个istream对象)。第一个参数是目标数组,第二个参数是数组长度。getline()使用它来避免超越数组的边界。
getline(cin,str);
这里没有使用句点表示法,这表明这个getline()不是类方法。它将cin作为参数,指出到哪里取查找输入。另外,也没有指出字符串长度的参数,因此string对象将根据字符串的长度自动调整自己的大小。
4.4 结构简介
C++中的结构是一种比数组更灵活的数据格式,因为同一个结构可以存储多种类型的数据,这使得能够将有关信息放在一个结构中,从而将数据的表示合并到一起。
结构也是C++OOP堡垒(类)的基石,结构的学习会离C++的核心OOP更近。
结构是用户定义的类型,而结构声明定义了这种类型的数据属性。定义了类型后,便可以创建这种类型的变量。因此创建结构包括两步,首先,定义结构描述:它描述并标记了能够存储在结构中的各种数据类型。然后按描述创建结构变量(结构数据对象)。
例如:假设Bloataire公司要创建一种类型来描述其生产线上充气产品的成员。具体地说,这种类型应存储产品名称、容量和售价。
struct inflatable // structure declaration
{
char name[20]; // 结构的成员
float volume;
double price;
}; // 结束结构声明
关键字struct表明,这些代码定义的是一个结构的布局。标识符inflatable是这种数据格式的名称,因此新类型的名称为inflatable。这样,便可以创建char或int类型的变量那样创建inflatable类型的变量了。接下来的大括号中包含的是结构存储的数据类型的列表,其中每个列表项都是一条声明语句。列表中的每一项都被称为结构成员。
定义结构后,便可以创建类型的变量了:
inflatable hat; // hat is a structure variable of type inflatable
inflatable woople_cushion; // type inflatable variable
inflatable mainframe; // type inflatable variable
如果熟悉C语言中的结构,则可能注意到:C++允许在声明结构变量时省略关键字 struct
struct inflatable goose; // keywora struct required in C
inflatable vincent; // keywora struct not required in C++
在C++中,结构标记的用法与基本类型名相同。这种变化强调的是,结构声明定义了一种新类型。在C++中,省略struct 不会错。
由于hat的类型为inflatable,因此可以使用成员运算符(.)来访问各个成员。例如:hat.volume指的是结构的volume成员,hat.price 指的是price成员。同样,vincent.price是vincent变量的price成员。总之,通过成员名能够访问结构的成员,就像通过索引能够访问数组的元素一样。
由于price成员被声明为double类型,因此hat.price 和vincent.price 相当于是double类型的变量,可以像使用常规double变量那样来使用它们。总之,hat是一个结构,而hat.price是一个double变量。
访问类成员函数(如cin.getline())的方式是从访问结构成员变量(如 vincent.price )的方式衍生而来的。
在程序中使用结构
#include <iostream>
using namespace std;
struct inflatable // structure declaration
{
char name[20];
float volume;
double price;
};
int main(){
inflatable guest =
{
"Glorious Gloria", // name value
1.88, // volume value
29.9 // price value
}; // guest is a structure variable of type inflatable
// It's initialized to the indicated values
inflatable pal =
{
"Audacious Arthur",
3.12,
32.99
}; // pal is a second variable of type inflatable
// NOTE: some implementations require using
// static inflatable guest =
cout << "Expand your guest list with " << guest.name;
cout << " and " << pal.name << "!\n";
cout << "You can have both for $";
cout << guest.price + pal.price << "!\n";
return 0;
}
运行结果:
Expand your guest list with Glorious Gloria and Audacious Arthur!
You can have both for $62.89!
程序说明:
结构声明的位置很重要。对于structur.cpp而言,有两种选择,可以将声明放在main()函数中,紧跟在开始括号的后面。另一种选择是将声明放到main()的前面,这里采用的便是这种方式,位于函数外面的声明被称为外部声明。
对于这个程序来说,两种选择之间没有实际区别,但是对于那些包含两个或更多的程序来说,差别很大。外部声明可以被其后面的任何函数使用,而内部声明只能被该声明所属的函数使用。通常应使用外部声明,这样所有函数都可以使用这种类型的结构。
变量也可以在函数内部和外部定义,外部变量由所有的函数共享。C++不提倡使用外部变量,但提倡使用外部结构声明。另外,在外部声明符号常量通常更合理。
请注意初始化方式:
inflatable guest =
{
"Glorious Gloria", // name value
1.88, // volume value
29.9 // price value
};
和数组一样,使用由逗号分隔值列表,并将这些值用花括号括起。在该程序中,每个值占一行,但也可以将它们全部放在同一行中。只是应用逗号将其分开:
inflatable guest ={"Glorious Gloria",1.88,29.9};
可以将结构的每个成员都初始化为适当类型的数据。例如:name成员是一个字符数组,因此可以将其初始化为一个字符串。
可将每个结构成员看作是相应类型的变量,因此,pal.price是一个double变量,而pal.name是一个char数组。当程序使用cout显示pal.name时,将把该成员显示为字符串。另外,由于pal.name是一个字符数组,因此可以用下标来访问其中的各个字符。例如:pal.name[0]是字符A。不过pal[0]没有意义,因为pal是一个结构,而不是数组。
C++11 结构初始化
与数组一样,C++11 也支持将列表初始化用于结构,切等号(=)是可选的:
inflatable duck {"Daphne",0.12,9.98};
其次,如果大括号内未包含任何东西,各个成员都将被设置为零。例如:下面的声明导致mayor.volume和mayor.price 被设置为零,且mayor.name的每个字节都将被设置为零:
inflatable mayor {};
最后,不允许缩窄转换。
结构可以将string 类作为成员吗?
可以将成员name指定为string对象而不是字符数组吗?
#include <string>
struct inflatable // structure definition
{
std::string name;
float volume;
double price;
};
答案是肯定的,只要使用的编译器支持对string对象作为成员的结构进行初始化。
一定要让结构定义能够访问名称空间std。为此,可将编译指令using移到结构定义之前;也可以像前面那样,将name的类型声明为std::string。
其他结构属性
C++使用用户定义的类型与内置类型尽可能相似。例如:可以将结构作为参数传递给函数,也可以让函数返回一个结构。另外,还可以使用赋值运算符(=)将 结构赋给另一个同类型的结构,这样结构中的每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。这种赋值被称为成员赋值(memberwise assignment)。
程序清单:
#include <iostream>
using namespace std;
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable bouquet =
{
"sunflowers",
0.20,
12.49
};
inflatable choice;
cout << "bouquet: " << bouquet.name << "for $";
cout << bouquet.price << endl;
choice = bouquet; // assign one structure to another
cout << "choice: " << choice.name << " for $";
cout << choice.price << endl;
return 0;
}
运行结果:
bouquet: sunflowersfor $12.49
choice: sunflowers for $12.49
从中可以看出,成员赋值是有效的,因为choice结构的成员值与bouquet结构中存储的值相同。
可以同时完成定义结构和创建结构变量的工作。为此,只需将变量名放在结束括号的后面即可:
struct perks
{
int key_number;
char car[12];
}mr_smith.ms_jones; // two perks variables
甚至可以初始化以这种方式创建的变量:
struct perks
{
int key_number;
char car[12];
}mr_glitz =
{
7, // value for mr_glitz.key_number member
"Packard" // value for mr_glitz.car member
};
然而,将结构定义和变量声明分开,可以使程序更易于阅读和理解。
还可以声明没有名称的结构类型,方法是省略名称,同时定义一种结构类型和一种这种类型的变量:
struct // no tag
{
int x; // 2 members
int y;
}position; // a structure variable
这样创建一个名为position的结构变量。可以使用成员运算符来访问它的成员(如:position.x),但这种类型没有名称,因此以后无法创建这种类型的变量。(一般不推荐)
结构数组
inflatable 结构包含一个数组(name)。也可以创建元素为结构的数组,方法和创建基本类型数组完全相同。例如:要创建一个包含100个inflatable结构的数组。
inflatable gifts[100]; // array of 100 inflatable structures
这样,gifts将是一个inflatable数组,其中的每个元素(如gifts[0]或gifts[99])都是inflatable对象,可以与成员运算符一起使用:
cin >> gifts[0].volume; // use volume member of first struct
cout << gifts[99].price << endl;// display price member of last struct
记住,gifts本身是一个数组,而不是结构,因此像gifts.price这样的表述是无效的。
要初始化结构数组,可以结合使用初始化数组的规则(用逗号分隔每个元素的值,并将这些值用花括号括起)和初始化结构的规则(用逗号分隔每个成员的值,并将这些值用花括号括起)。由于数组中的每个元素都是结构,因此可以使用结构初始化的方式来提供它的值。
因此,最终结果为一个被括在花括号中、用逗号分隔的值列表,其中每个值本身又是一个被括在花括号中、用逗号分隔的值列表:
inflatable gueests[2] = //initializing an array of structs
{
{"Bambi",0.5,21.99}, // first structure in array
{"Godzilla",2000,565.99} // next structure in array
};
程序清单:
#include <iostream>
using namespace std;
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable guests[2] = //initializing an array of structs
{
{"Bambi",0.5,21.99}, // first structure in array
{"Godzilla",2000,565.99} // next structure in array
};
cout << "The guests " << guests[0].name << " and " << guests[1].name
<< "\nhave a combined volume of "
<< guests[0].volume + guests[1].volume << " cubic feet.\n";
return 0;
}
运行结果:
The guests Bambi and Godzilla
have a combined volume of 2000.5 cubic feet.
结构中的位字段
与C语言一样,C++也允许指定占用特定位数的结构成员,这使得创建与某个硬件设备上的寄存器对应的数据结构非常方便。字段的类型应为整型或枚举,接下来是冒号,冒号后面是一个数字,它指定了使用的位数。可以使用没有名称的字段来提供间距。每个成员都被称为位字段(bit field)。
struct torgle_register
{
unsigned int SN : 4; // 4 bits for SN value
unsigned int : 4; // 4 bits unused
bool goodIn : 1; // valid input (1 bit)
bool goodTorgle : 1; // successful torgling
};
可以像通常那样初始化这些字段,还可以使用标准的结构表示法来访问位字段:
torgle_register tr = {14,_true,false};
if(tr.goodIN)
位字段通常用在低级编程中。
4.5 共用体
共用体(union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。也就是说,结构可以同时存储int、long和double,共用体只能存储int、long或double。共用体的句法与结构相似,但含义不同。
union one4all
{
int int_val;
long long_val;
double double_val;
};
可以使用one4all变量来存储int、long或double,条件是在不同的时间进行:
one4all pail;
pail.int_val = 15; // store an int
cout << pail.int_val;
pail.double_val = 1.38; // store a double , int value is lost
cout << pail.double_val;
因此,pail有时可以是int变量,而有时又可以是double变量。成员名称标识了变量的容量。由于共用体每次只能存储一个值,因此它必须有足够的空间存储最大的成员,所以,共用体的长度为其最大成员的长度。
共用体的用途之一是:当数据项使用两种或者更多种格式(但不会同时使用)时,可节省空间。例如:假设管理一个小商品目录,其中有一些商品的ID为整数,而另一些的ID为字符串,在这种情况下,可以这样做:
struct widget
{
char brand[20];
int type;
union id // format depends on widget type
{
long id_num; // type 1 widgets
char id_char[20]; // other widgets
}id_val;
};
widget prize;
if(prize.type == 1)
cin >> prize.id_val.id_num;
else
cin >> prize.id_val.id_char;
匿名共用体(anonymous union)没有名称,其成员将成为位于相同地址处的变量。显然,每次只有一个成员是当前的成员:
struct widget
{
char brand[20];
int type;
union // anonymous union
{
long id_num; // type 1 widgets
char id_char[20]; // other widgets
};
};
widget prize;
if(prize.type == 1)
cin >> prize.id_num;
else
cin >> prize.id_char;
由于共用体是匿名的,因此id_num和id_char被视为prize的两个成员,它们的地址相同,所以不需要中间标识符id_val。
共用体常用于(但并非只能用于)节省内存。
4.6 枚举
C++的enum工具提供了另一种创建符号常量的方式,这种方式可以代替const。它还允许定义新类型,但必须按严格的限制进行。使用enum的句法与使用结构相似。
enum spectrum {red,orange,yellow,green,bule,violet,indigo,ultraviolet};
这条语句完成两项工作:
- 让spectrum成为新类型的名称:spectrum被称为枚举(enumeration),就像struct变量被称为结构一样。
- 将red,orange,yellow等作为符号常量,它们对应的整数值0~7。这些常量叫作枚举量(enumerator)。
在默认情况下,将整数值赋给枚举量,第一个枚举量的值为0,第二个枚举量的值为1,依次类推。可以通过显式地指定整数值来覆盖默认值。
可以用枚举名来声明这种类型的变量:
spectrum band; // band a variable of type spectrum
枚举变量具有一些特殊的属性。
在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋值给这种枚举的变量。
band = blue; // valid,blue is an enumerator
band = 2000; // invalid, 2000 not an enumerator
因此,spectrum变量受到限制,只有8个可能的值。如果试图将一个非法值赋给它,则有些编译器将出现编译器错误,而另一些则发出警告。为获得最大限度的可移植性,应将把非enum值赋给enum变量视为错误。
对于枚举,只定义了赋值运算符。具体来说,没有为枚举定义算术运算:
band = orange; // valid
++band; // not valid
band = orange + red; // not valid,but a little tricky
枚举量是整型,可被提升为int类型,但int类型不能自动转换为枚举类型。
int color = blue; // valid,spectrum type promoted to int
band = 3; // invalid,int not converted to spectrum
color = 3 + red; // valid,red converted to int
枚举的规则相当的严格。实际上,枚举更常被用来定义相关的符号常量,而不是新类型。例如:可以用枚举来定义switch语句中使用的符号常量。如果打算只使用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称。
enum {red,orange,yellow,green,bule,violet,indigo,ultraviolet};
设置枚举量的值 可以使用赋值运算符来显式地设置枚举量的值:
enum bits{one = 1, two = 2, four = 4, eight = 8};
指定的值必须是整数,也可以只显式地定义其中一些枚举量的值:
enum bigstep{first,second = 100,third};
这里,first在默认情况下为0。后面没有被初始化的枚举量的值将比其前面的枚举量大1.因此,third的值为101。
最后,可以创建多个值相同的枚举量:
enum {zero,null = 0,one,numero_uno = 1};
其中,zero和null都为0,one和numero_uno都为1。
枚举的取值范围
最初,对于枚举来说,只有声明中指出的那些值是有效的,然而,C++现在通过强制类型转换,增加了可赋给枚举变量的合法值。每个枚举都有取值范围(range),通过强制类型转换,可以将取值范围中的任何数值赋给枚举变量,即使这个值不是枚举值。例如:
enum bits{one = 1, two = 2, four = 4, eight = 8};
bits myflag;
则下面写法是合法的:
myflag = bits(6); // valid,because 6 is in bits range
其中6不是枚举值,但是它位于枚举定义的取值范围内。
取值范围的定义如下:首先,要找出上限,需要指定枚举量的最大值。找到这个最大值的、最小值的2的幂,将它减去1,得到的便是取值范围的上限。
例如:前面定义的bigstep的最大值枚举值是101。在2的幂中,比这个数大的最小值为128,因此取值范围的上限为127。
要计算下限,需要知道枚举量的最小值。如果它不小于0,则取值范围的下限为0;否则,采用与寻找上限方式相同的方式,但加上负号。
例如:如果最小的枚举量为-6,而比它小的、最大的2的幂是-8(加上负号),因此下限为-7。
选择用多少空间来存储枚举由编译器决定。
4.7 指针和自由存储空间
指针是一个变量,其存储的值是地址,而不是值本身。
在学习指针之前,先了解如何找到常规变量的地址。只需对变量应用地址运算符(&),就可以获得它的位置。
程序清单:
#include <iostream>
using namespace std;
int main(){
int donuts = 6;
double cups = 4.5;
cout << "donuts value = " << donuts;
cout << " and donuts address = " << &donuts << endl;
cout << "cups value = " << cups;
cout << " and cups address = " << &cups << endl;
return 0;
}
运行结果:
donuts value = 6 and donuts address = 0x61fe1c
cups value = 4.5 and cups address = 0x61fe10
显示地址时,该实现的cout使用十六进制表示法,因为这是常用于描述内存的表示法。
使用常规变量时,值是指定的量,而地址为派生量。
处理存储数据的新策略刚好相反,将地址视为指定的量,而将值视为派生量。一种特殊类型的变量——指针,用于存储值的地址。因此,指针名表示的是地址。* 运算符被称为间接值(indirect value)或解除引用(dereferencing)运算符,将其应用于指针,可以得到该地址处存储的值。
例如:假设manly是一个指针,则manly表示的是一个地址,而*manly 表示存储在该地址处的值。*manly 与常规int变量等级。
程序清单:
#include <iostream>
using namespace std;
int main()
{
int updates = 6; // declare a variable
int * p_updates; // declare pointer to an int
p_updates = &updates; // assign address of int to pointer
//express values two ways
cout << "Values: updates = " << updates;
cout << ", *p_updates = " << *p_updates << endl;
// express address two ways
cout << "Addresses:&updates = " << &updates;
cout << ",p_updates = " << p_updates << endl;
// use pointer to change value
*p_updates = *p_updates + 1;
cout << "Now updates = " << updates << endl;
return 0;
}
运行结果:
Values: updates = 6, *p_updates = 6
Addresses:&updates = 0x61fe14,p_updates = 0x61fe14
Now updates = 7
从中可知,int变量updates 和指针变量p_updates只不过是同一枚银币的两面。变量updates表示值,并使用&运算符来获得地址;而变量p_updates表示地址,并使用* 运算符来获得值。由于p_updates指向updates,因此*p_updates 和updates完全等价。可以像使用int变量那样使用 *p_updates 。甚至可以将值赋给 *p_updates 。这样做将修改指向的值,即updates。
声明和初始化指针
计算机需要跟踪指针指向的值的类型。例如:char的地址与double的地址,看上去没什么两样,但char和double使用的字节数是不同的,它们存储值时使用的内部格式也是不同的。因此,指针声明必须指定指针指向的数据的类型。
例如:
int * p_updates;
这表明,*p_updates 的类型为int。由于*运算符 被用于指针,因此p_updates 变量本身必须是指针。
我们说p_updates指向int类型,还可以说p_updates的类型是指向int的指针,或int* 。
可以这样说,p_updates是指针(地址),而*p_updates 是int,而不是指针。
注意:*运算符 两边的空格是可选的。传统上,C程序员使用这种格式:
int *ptr;
这强调 *ptr 是一个int类型的值。而很多C++程序员使用这种格式:
int* ptr;
这强调的是:int* 是一种类型——指向int的指针。
在哪里添加空格对于编译器来说没有任何区别,甚至可以这样:
int*ptr;
但要知道的是,下面的声明创建一个指针(p1)和一个int变量(p2):
int* p1,p2;
对每个指针变量名,都需要使用一个*
注意:在C++中,int* 是一种复合类型,是指向int的指针。
可以用同样的句法来声明指向其他类型的指针:
double * tax_ptr; // tax_ptr points to type double
char * str; // str points to type char
由于已将tax_ptr声明为一个指向double的指针,因此编译器知道*tax_ptr 是一个double 类型的值。也就是说,它知道*tax_ptr 是一个以浮点数格式存储的值,这个值(在大多数系统上)占据8个字节。指针变量不仅仅是指针,而且是指向特定类型的指针。tax_ptr的类型是指向double的指针(或double * 类型),str是指向char的指针类型(或char* )。尽管它们都是指针,却是不同类型的指针。和数组一样,指针都是基于其他类型的。
虽然tax_ptr和str指向两种长度不同的数据类型,但这两个变量本身的长度通常是相同的。也就是说,char的地址和double的地址长度相同。这就好比1016可能是超市的街道地址,而1024可以是小村庄的街道地址一样。地址的长度或值既不能指示关于变量的长度或类型的任何信息,也不能指示该地址上有什么建筑物。
一般来说,地址需要2个还是4个字节,取决于计算机系统。
可以在声明语句中初始化指针,在这种情况下,被初始化的是指针,而不是它指向的值。也就是说,下面的语句将pt(而不是*pt )的值设置为 &higgens:
int higgens = 5;
int * pt = &higgens;
程序清单:
#include <iostream>
using namespace std;
int main()
{
int higgens = 5;
int * pt = &higgens;
cout << "Value of higgens = " << higgens
<< "; Address of higgens = " << &higgens << endl;
cout << "Value of *pt = " << *pt
<< "; Value of pt = " << pt << endl;
return 0;
}
运行结果:
Value of higgens = 5; Address of higgens = 0x61fe14
Value of *pt = 5; Value of pt = 0x61fe14
从中可知,程序将pt(而不是 *pt )初始化为higgens的地址。(每个人的系统上,显示的地址可能不同,显示格式也可能不同)
指针的危险
危险更容易发生在那些使用指针不仔细的人身上。极其重要的一点是:在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤,忽略这一步无疑是自找麻烦。
long * fellow; // create a pointer to long
*fellow = 223323; // place a value in never-never land
fellow 确实是一个指针,但是它指向哪里呢?上述代码没有将地址赋给fellow。那么223323将被放在哪里呢?根本不知道。由于fellow没有被初始化,它可能有任何值。不管值是什么,程序都将它解释为存储223323的地址。如果fellow的值碰巧为1200,计算机将把数据放在地址1200上,即使这恰巧是程序代码的地址。fellow指向的地方很可能并不是所要存储223323的地方。这种错误可能会导致一些最隐匿、最难以跟踪的bug。
警告:一定要在对指针应用解除引用运算符(*) 之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律。
指针和数字
指针不是整型的,虽然计算机通常把地址当作整数来处理。从概念上看,指针与数组是截然不同的类型。整数是可以执行加、减、除等运算的数字,而指针描述的是位置,将两个地址相乘没有任何意义。从可以对整数和指针执行的操作上看,它们也是彼此不同的。因此,不能简单地将整数赋给指针。
int * pt;
pt = 0xB8000000; // type mismatch
在这里,左边是指向int的指针,因此可以把它赋给地址,但右边是一个整数。0xB8000000是老式计算机系统中视频内存的组合段偏移地址,但这条语句并没有告诉程序,这个数字就是一个地址。在C99标准发布之前,C语言允许这样赋值。但C++在类型一致方面的要求更严格,编译器将显示一条错误消息,通告类型不匹配。要将数字值作为地址来使用,应通过强制类型转换将数字转换适当的地址类型。
int * pt;
pt = (int *)0xB8000000; // type now match
这样,赋值语句的两边都是整数的地址,因此这样赋值有效。
注意:pt是int值的地址并不意味着pt本身的类型是int。例如:在有些平台中,int类型是个2字节值,而地址是个4字节值。
使用new来分配内存
对指针的工作方式有一定的了解后,再看看它如何实现在程序运行时分配内存。前面我们都将指针初始化为变量的地址;变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。==指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。==在C语言中,可以用库函数malloc()来分配内存;在C++中仍可以这样做,但C++还有更好的方法——new运算符。
在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。这里的关键所在是C++的new运算符。程序员要告诉new,需要为哪种数据分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针。
int * pn = new int;
new int告诉程序,需要适合存储int的内存。new 运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。接下来将地址赋给pn,pn是被声明为指向int的指针。现在,pn是地址,而*pn 是存储在那里的值。将这种方法与将变量的地址赋给指针进行比较:
int higgens;
int * pt = &higgens;
在这两种情况下(pn和pt)下,都是将一个int变量的地址赋给了指针。在第二种情况下,可以通过名称higgens来访问该int,在第一种情况下,则只能通过该指针进行访问。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式:
typeName * pointer_name = new typeName;
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。当然,如果已经声明了相应类型的指针,则可以使用该指针,而不用再声明一个新的指针。
程序清单:
#include <iostream>
using namespace std;
int main(){
int nights = 1001;
int * pt = new int;
*pt = 1001;
cout << "nights value = ";
cout << nights << ":location " << &nights << endl;
cout << "int ";
cout << " value = " << *pt << " : location = " << pt << endl;
double * pd = new double;
*pd = 10000001.0;
cout << "double ";
cout << "value = " << *pd << ":location = " << pd << endl;
cout << "location of pointer pd: " << &pd << endl;
cout << "size of pt = " << sizeof(pt);
cout << ": size of *pt = " << sizeof(*pt) << endl;
cout << "size of pd = " << sizeof pd;
cout << ": size of *pd = " << sizeof(*pd) << endl;
return 0;
}
运行结果:
nights value = 1001:location 0x61fe14
int value = 1001 : location = 0xea1870
double value = 1e+007:location = 0xea1890
location of pointer pd: 0x61fe08
size of pt = 8: size of *pt = 4
size of pd = 8: size of *pd = 8
当然,内存位置的准确值随系统而异。
程序说明:
该程序使用new分别为int类型和double类型的数据对象分配内存。这是在程序运行时进行的。指针pt和pd指向这两个数据对象,如果没有它们,将无法访问这些内存单元。有了这两个指针,就可以像使用变量那样使用*pt 和 *pd ,从而将这些值赋给新的数据对象。同样,可以通过打印 *pt 和 *pd 来显示这些值。
该程序还指出了必须声明指针所指向的类型的原因之一。地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。从这两个值的地址可以知道,它们都只是数字,并没有提供类型或长度信息。另外,指向int的指针的长度与指向double的指针相同,它们都是地址。
对于指针,需要指出的另一点是,new分配的内存块通常与常规变量声明分配的内存块不同。变量nights和pd的值都存储在被称为栈(stack)的内存区域中,而new从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。
在C++中,值为0的指针被称为空指针(null pointer)。C++确保空指针不会指向有效的数据,因此它常被用来表示运算符或函数失败(如果成功,它们将返回一个有用的指针)。
使用delete释放内存
当需要内存时,可以使用new来请求。这只是C++内存管理数据包中有魅力的一个方面。另一方面是delete运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一步。归还或释放(free)的内存可供程序的其他部分使用。使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的):
int * ps = new int; // allocate memory with new
...... //use the memory
delete ps; // free memory with delete when done
这将释放ps指向的内存,但不会删除指针ps本身。
例如:可以将ps重新指向另一个新分配的内存块。一定要配对地使用new和delete;否则将发生内存泄露(memory leak),也就是说,被分配的内存再也无法使用了。如果内存泄露严重,则程序将由于不断寻找更多内存而终止。
不雅尝试释放已经释放的内存块,C++指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用delete来释放声明变量所获得的内存:
int * ps = new int ; //ok
delete ps; //ok
delete ps; //not ok now
int jugs = 5; //ok
int * pi = &jugs; //ok
delete pi; //not allowed,memory not allocated by new
警告:只能用delete来释放使用new分配的内存。然而,对空指针使用delete是安全的。
注意,使用delete的关键在于,将它用于new分配的内存。这并不意味着要使用用于new的指针,而是用于new的地址。
int * ps = new int; // allocate memory
int * pq = ps; // set second pointer to same block
delete pq; // delete with second pointer
一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。
使用new来创建动态数组
如果程序只需要一个值,则可能会声明一个简单变量,因为对于管理一个小型数据对象来说,这样做比使用new和指针更简单,尽管给人留下的印象不那么深刻。通常,对于大型数据(如数组、字符串和结构),应使用new,这正是new的用武之地。
例如,假设要编写一个程序,它是否需要数组取决于运行时用户提供的信息。如果通过声明来创建数组,则在程序被编译时将它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。但使用new时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫作动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。
动态数组的两个基本问题:如何使用C++的new运算符创建数组以及如何使用指针访问数组元素。
使用new创建动态数组
在C++中,创建动态数组很容易,只要将数组的元素类型和元素数目告诉new即可。必须在类型名后面加上方括号,其中包含元素数目。
例如:要创建一个包含10个int元素的数组,可以这样做:
int * psome = new int[10]; // get a block of 10 ints
new 运算符返回第一个元素的地址。该地址被赋给指针psome。
当程序使用完new分配的内存块时,应使用delete释放它们。然而,对于使用new创建的数组,应使用另一种格式的delete来释放:
delete [] psome; // free a dynamic array
方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。请注意delete和指针之间的方括号。==如果使用new时,不带方括号,则使用delete时,也不带方括号。如果使用new时带方括号,则使用delete时也带方括号。==C++的早期版本无法识别方括号表示法。然而,对于ASNI/ISO标准来说,new与delete的格式不匹配导致的后果是不确定的,这意味着程序员不能依赖于某种特定的行为。
int * pt = new int;
short * ps = new short[500];
delete [] pt; // effect is undefined,don't do it
delete ps; // effect is undefined,don't do it
总之,使用new’和delete时,应遵循以下规则:
- 不要使用delete来释放不是new分配的内存。
- 不要使用delete释放同一个内存块两次。
- 如果使用new [ ] 为数组分配内存,则应使用delete [ ] 来释放。
- 如果使用new [ ] 为一个实体分配内存,则应使用delete(没有方括号)来释放。
- 对空指针应用delete是安全的。
psome是指向一个int(数组第一个元素)的指针,您的责任是跟踪内存块中的元素个数。也就是说,由于编译器不能对psome是指向10个整数中的第1个这种情况进行跟踪,因此编写程序时,必须让程序跟踪元素的数组。
实际上,程序确实跟踪了分配的内存量,以便以后使用delete [ ]运算符时能够正确地释放这些内存。但这种信息不是公用的。例如:不能使用sizeof运算符来确定动态分配的数组包含的字节数。
为数组分配内存的通用格式如下:
type_name * pointer_name = new type_name [num_elements];
使用new运算符可以确保内存块足以存储num_elements个类型为type_name的元素,而pointer_name将指向第1个元素。
使用动态数组
例如:创建指针psome,它指向包含10个int值的内存快中的第1个元素:
int * psome = new int[10]; // get a block of 10 ints
可以将它看作是一根指向该元素的手指。假设int占4个字节,则将手指沿着正确的方向移动4个字节,手指将指向第2个元素。总共有10个元素,这就是手指的移动范围,因此,new语句提供了识别内存块中每个元素所需的全部信息。
现在从实际角度考虑这个问题。如何访问其中的元素呢?第一个元素不成问题。由于psome指向数组的第1个元素,因此*psome 是第1个元素的值。这样,还有9个元素。如果没有使用过C语言,下面这种方法比较简单:只要把指针当作数组名使用即可。也就是说,对于第1个元素。可以使用psome[0],而不是*psome ;对于第2个元素,可以使用psome[1],依此类推。这样,使用指针来访问动态数组就非常简单了。可以这样做的原因是:C和C++内部都使用指针来处理数组。数组和指针基本等价于C和C++的优点之一。
程序清单:
#include <iostream>
using namespace std;
int main()
{
double * p3 = new double[3]; // space for 3 doubles
p3[0] = 0.2; // treat p3 like an array name
p3[1] = 0.5;
p3[2] = 0.8;
cout << "ps[1] is " << p3[1] << ".\n";
p3 = p3 + 1; // increment the pointer
cout << "Now p3[0] is " << p3[0] << " and ";
cout << "p3[1] is " << p3[1] << ".\n";
p3 = p3 - 1; // point back to begining
delete [] p3; // free the memory
return 0;
}
运行结果:
ps[1] is 0.5.
Now p3[0] is 0.5 and p3[1] is 0.8.
从中可知,是将指针p3当作数组名来使用,p3[0]为第1个元素,依次类推。
下面这行代码指出数组名和指针之间的根本差别:
p3 = p3 + 1; // okay for pointers,wrong for array names
不能修改数组名的值,但指针是变量,因此可以修改它的值。请注意将p3加1的效果:表达式p3[0]现在值的是数组的第2个值,因此,将p3加1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给delete[ ] 提供正确的地址。
相邻的int地址通常相差2个字节或4个字节,而将p3加1后,它将指向下一个元素的地址,这表明指针算术有一些特别的地方。
指针、数组和指针算术
指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。首先,将整数变量加1后,其值将增加1;但**将指针变量加1后,增加的量等于它指向的类型的字节数。**将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。
程序清单:
//addpntrs.cpp --- pointer addition
#include <iostream>
using namespace std;
int main()
{
double wages[3] = {10000.0,20000.0,30000.0};
short stacks[3] = {3,2,1};
//Here are two ways to get the address of an array
double * pw = wages; // name of an array = address
short * ps = &stacks[0]; // or use address operator
// with array element
cout << "pw = " << pw << ", *pw = " << *pw << endl;
pw = pw + 1;
cout << "add 1 to the pw pointer:\n";
cout << "pw = " << pw << ",*pw = " << *pw << "\n\n";
cout << "ps = " << ps << ",*ps = " << *ps << endl;
ps = ps + 1;
cout << "add 1 to the ps pointer:\n";
cout << "ps = " << ps << ",*ps = " << *ps << "\n\n";
cout << "access two elements with array notation\n";
cout << "stacks[0] = " << stacks[0]
<< ", stacks[1] = " << stacks[1] << endl;
cout << "access two elements with pointer notation\n";
cout << "*stacks = " << *stacks
<< ", *(stacks + 1) = " << *(stacks + 1) << endl;
cout << sizeof(wages) << " = size of wages array\n";
cout << sizeof(pw) << " = size of pw pointer\n";
return 0;
}
运行结果:
pw = 0x61fdf0, *pw = 10000
add 1 to the pw pointer:
pw = 0x61fdf8,*pw = 20000
ps = 0x61fdea,*ps = 3
add 1 to the ps pointer:
ps = 0x61fdec,*ps = 2
access two elements with array notation
stacks[0] = 3, stacks[1] = 2
access two elements with pointer notation
*stacks = 3, *(stacks + 1) = 2
24 = size of wages array
8 = size of pw pointer
程序说明: 在多数情况下,C++将数组名解释为数组第1个元素的地址。因此,将pw声明为指向double类型的指针,然后将它们初始化为wages——wages数组中第1个元素的地址:
double * pw = wages;
和所有数组一样,wages也存在下面的等式:
wages = &wages[0] = address of first element of array
接下来,程序查看pw和*pw 的值。前者是地址,后者是存储在该地址中的值。由于pw指向第1个元素,因此*pw 显示的值为第1个元素的值,即10000。接着,程序将pw加1。正如前面指出的,这样数字地址值将增加8,这使得pw的值为第2个元素的地址,因此*pw 现在的值是20000——第2个元素的值。
注意:将指针变量加上后,其增加的值等于指向的类型占用的字节数。
现在来看下数组表达式stacks[1]。C++编译器将该表达式看作*(stacks + 1) ,这意味着先计算数组第2个元素的地址,然后找到存储在那里的值。最后的结果便是stacks[1]的含义。
从该程序的输出可知,*(stacks + 1) 和stacks[1]是等价的。同样,*(stacks + 2) 和stacks[2]也是等价的。
通常,使用数组表示法时,C++都执行如下的转换:
arrayname[i] becomes *(arrayname + i)
如果使用的是指针,而不是数组名,则C++也将执行同样的转换:
pointername[i] becomes *(pointername + i)
因此,在很多情况下,可以相同的方式使用指针和数组名。对于它们,可以使用数组括号表示法,也可以使用解除引用运算符(*) 。在多数表达式中,它们都表示地址。区别之一是:可以修改指针的值,而数组名是常量。
pointername = pointername + 1; // valid
arrayname = arrayname + 1; // not valid
另一个区别是:对数组应用sizeof运算符得到的是数组的长度,而对于指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组。
数组的地址
对数组取地址时,数组名也不会被解释为其地址。等等,数组名难道不被解释为数组的地址吗?不完全如此:数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址:
short tell[10]; // tell an array of 20 bytes
cout << tell << endl; // dispalys &tell[0]
cout << &tell << endl; // displays address of whole array
从数字上说,这两个地址相同;但从概念上说,&tell[0] (即tell)是一个2字节内存快的地址,而&tell 是一个20字节内存快的地址。因此,表达式tell + 1 将地址值加2,而表达式 &tell + 2 将地址加20。换句话说,tell是一个short指针 (*short ),而&tell是一个这样的指针,即指向包含20个元素的short数组(short(*)[20] )。
可能会问,前面有关&tell的类型描述是如何来的呢?首先,可以这样声明和初始化这种指针:
short (*pas)[20] = &tell; // pas points to array of 20 shorts
如果要描述变量的类型,可将声明中的变量名删除。因此,pas的类型为short(*)[20] 。
由于pas被设置为&tell,因此*pas 与tell等价,所以(*pas)[0] 为tell数组的第一个元素。
指针小结
-
声明指针 要声明指向特定类型的指针:typeName * pointerName; double * pn; // pn can point to a double value
char * pc; // pc can point to a char value
其中,pn和pc都是指针,而double * 和char * 是指向double的指针和指向char的指针。 -
给指针赋值 应将内存地址赋给指针,可以对变量名应用&运算符 ,来获得被命名的内存的地址,new运算符返回未命名的内存的地址。 double * pn;
double * pa;
char * pc;
double bubble = 3.2;
pn = &bubble; // assign address of bubble to pn
pc = new char; // assign address of newly allocated char memory to pc
pa = new double[30]; // assign address of 1st element of array of 30 double to pa
-
对指针解除引用 对指针解除引用意味着获得指针指向的值。对指针应用解除引用或间接值运算符(*) 来解除引用。因此,上面的例子中:pn是指向bubble的指针,则 * pn 是指向的值,即3.2。 例如: cout << *pn; // print the value of bubble
*pc = 'S'; // place 'S' into the memory location whose address is pc
另一种对指针解除引用的方法是使用数组表示法,例如:pn[0] 与 *pn 是一样的。决不要对未被初始化为适当地址的指针解除引用。 -
区分指针和指针所指向的值 如果pt是指向int的指针,则*pt 不是指向int的指针,而是完全等同于一个int类型的变量。pt才是指针。 int * pt = new int;
*pt = 5;
-
数组名 在多数情况下,C++将数组名视为数组的第一个元素的地址。 int tacos[10]; // now tacos is the same as &tacos[0]
一种例外情况是,将sizeof运算符用于数组名用时,此时将返回整个数组的长度(单位为字节)。 -
指针算术 C++允许将指针和整数相加。加1的结果等于原来的地址值加上指向的对象占用的总字节数。还可以将一个指针减去另一个指针,或得两个指针的差。后一种运算将得到一个整数,仅当两个指针指向同一个数组(也可以指向超出结尾的一个位置)时,这种运算才有意义;这将得到两个元素的间隔。 int tacos[10] = {5,2,8,4,1,2,2,4,6,8};
int * pt = tacos; // suppose pt and tacos are the address 3000
pt = pt + 1; // now pt is 3004 if a int is 4 bytes
int *pe = &tacos[9]; // pe is 3036 if an int is 4 bytes
pe = pe - 1; // now pe is 3032,the address of tacos[8]
int diff = pe - pt;
// diff is 7,the separation between tacos[8] and tacos[1]
-
数组的动态联编和静态联编 使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置: int tacos[10]; // static binding ,size fixed at compile time
使用new[ ] 运算符创建数组时,将采用动态联编(动态数组)。即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应采用delete[ ] 释放其占用的内存。 int size;
cin >> size;
int * pz = new int[size]; // dynamic binding,size set at run time
...
delete [] pz; // free memory when finished
-
数组表示法和指针表示法 使用方括号数组表示法等同于对指针解除引用: tacos[0] means * tacos means the value at address tacos
tacos[3] means *(tacos + 3) means the value at address tacos + 3
数组名和指针变量都是如此,因为对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。 int * pt = new int[10]; // pt points to block of 10 ints
*pt = 5; // set (element number 0) to 5
pt[0] = 6; // reset (element number 0) to 6
pt[9] = 44; // set tenth element (element number 9) to 44
int coats[10];
*(coats + 4) + 12; // set coats[4] to 12
指针和字符串
数组和指针的特殊关系可以扩展到C-风格字符串。
char flower[10] = "rose";
cout << flower << "s are red \n";
数组名是第一个元素的地址,因此cout语句中的flower是包含字符r的char元素的地址。
cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0 )为止。
总之,如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。
对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。
注意:在cout和多数C++表达式中,char数组名、char指针以及用引号括起来的字符串常量都被解释为字符串第一个字符的地址。
程序清单:
#include <iostream>
#include <cstring> // declare strlen(),strcpy()
using namespace std;
int main()
{
char animal[20] = "bear"; // animal holds bear
const char * bird = "wren"; // bird holds address of string
char * ps; // uninitialized
cout << animal << " and "; // display bear
cout << bird << "\n"; // display wren
cout << "Enter a kind of animal: ";
cin >> animal; // ok if input < 20 chars
// cin >> ps; Too horrible a blunder to try;
//ps doesn't point to allocated space
ps = animal; // set ps to point to string
cout << ps << "!\n"; // ok, same as using animal
cout << "Before using strcpy():\n";
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int *)ps << endl;
ps = new char[strlen(animal) + 1]; // get new storage
strcpy(ps,animal); // copy string to new storage
cout << "After using strcpy():\n";
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int *)ps << endl;
delete [] ps;
return 0;
}
注意:在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用new初始化过的指针。
一般来说,如果给cout提供一个指针,它将打印地址。但如果指针的类型为char* ,则cout将显示指向的字符串。如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如int* 。
使用new创建动态结构
在运行时创建数组优于在编译时创建数组,对于结构也是如此。需要在程序运行时为结构分配所需的空间,这可以使用new运算符来完成。由于类与结构非常相似,因此有关结构的技术也适用于类。
将new用于结构由两步组成:创建结构和访问其成员。要创建结构,需要同时使用结构类型和new。例如:要创建一个未命名的inflatable类型,并将其地址赋值给一个指针:
inflatable * ps = new inflatable;
这将把足以存储inflatable结构的一块可用内存的地址赋给ps。这种语法和C++的内置类型完全相同。
比较棘手的是访问成员。创建动态结构时,不能将成员运算符句点用于结构名,因为这种结构没有名称,只知道它的地址。C++专门为这种情况提供了一个运算符:箭头成员运算符(->) 。该运算符由连字符和大于号组成,可用于指向结构的指针,就像点运算符可用于结构名一样。
例如:如果ps指向一个inflatable结构,则ps->price是被指向的结构的price成员。
struct things
{
int good;
int bad;
};
things grubnose = {3,453}; //grubnose是一个结构
things * pt = &grubnose; //pt 指向结构 grubnose structure
grubnose.good grubnose.bad //将运算符.用于结构名
3 453 //结构 grubnose
pt->good pt->bad //将运算符->用于指向结构的指针
提示:有时,在指定结构成员时,搞不清楚何时应使用句点运算符,何时应使用箭头运算符。
规则很简单:如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构的指针,则使用箭头运算符。
另一种访问结构成员的方法是:如果ps是指向结构的指针,则*ps 就是被指向的值——结构的本身。由于*ps 是一个结构,因此(*ps ).price是该结构的price成员。
程序清单:
#include <iostream>
using namespace std;
struct inflatable // structure definition
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable * ps = new inflatable; // allot memory for structure
cout << "Enter name of inflatable item: ";
cin.get(ps->name,20); // method 1 for member access
cout << "Enter volume in cubic feet:";
cin >> (*ps).volume; // method 2 for member access
cout << "Enetr price: $";
cin >> ps->price;
cout << "Name: " << (*ps).name << endl; // method 2
cout << "Volume: " << ps->volume << " cubic feet\n"; // method 1
cout << "Price: $" << ps->price << endl; // method 1
delete ps; // free memory used by structure
return 0;
}
运行结果:
Enter name of inflatable item: Fabulous Frodo
Enter volume in cubic feet:1.4
Enetr price: $27.99
Name: Fabulous Frodo
Volume: 1.4 cubic feet
Price: $27.99
上面的程序是两种访问结构成员的指针表示法。
🚀一个使用new和delete的示例:
介绍一个使用new’和delete来存储通过键盘输入的字符串的示例。定义一个函数getname(),该函数返回一个指向输入字符串的指针。该函数将输入读入到一个大型的临时数组,然后用new[ ] 创建一个刚好能够存储该输入字符串的内存块,并返回一个指向该内存块的指针。对于读取大量字符串的程序,这种方法可以节省大量内存(实际编写程序时,使用string类将更容易,因为这样可以使用内置的new和delete)。
#include <iostream>
#include <cstring> // or string.h
using namespace std;
char * getname(void); // function prototype
int main()
{
char * name; // create pointer but no storage
name = getname(); // assign address of string to name
cout << name << " at " << (int *)name << "\n";
delete [] name; // memory freed
name = getname(); // reuse freed memory
cout << name << " at " << (int *)name << "\n";
delete [] name; // memory freed again
return 0;
}
char * getname() //return pointer to new string
{
char temp[80]; // temporary storage
cout << "Enter last name: ";
cin >> temp;
char * pn = new char[strlen(temp) + 1];
strcpy(pn,temp); // copy string into smaller space
return pn; // temp lost when function ends
}
运行结果:
Enter last name: Fredeldumpkin
Fredeldumpkin at 0x004326b8
Enter last name: Pook
Pook at 0x004301c8
程序说明:
函数getname()使用int将输入的单词放到temp数组中,然后使用new分配新新内存,以存储该单词。程序需要strle(temp) + 1个字符(包括空字符)来存储该字符串,因此将这个值提供给new。获得空间后,getname()使用标准库函数strcpy()将temp中的字符串复制到新的内存块中。该函数并不检查内存块是否能够容纳字符串,但getname()通过使用new请求合适的字节数来完成了这样的工作。最后,函数返回了pn,这是字符串副本的地址。
在这个程序中,getname()分配内存,而main()释放内存。将new和delete放在不同的函数中通过并不是个好办法,因为这样很容易忘记使用delete。
自动存储、静态存储和动态存储
-
自动存储 在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable),这意味着它们在所属的函数被调用时自动产生,在函数结束时消亡。例如:上面程序中temp数组仅当getname()函数活动时存在,当程序控制权回到main()时,temp使用的内存将自动被释放。如果getname()返回temp的地址,则main()中的name指针指向的内存将很快得到重新使用。 实际上,自动变量是一个局部变量,其作用域为包含它的代码块。 自动变量通常存储在栈中,这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这就被称为后进先出(LIFO)。 -
静态存储 静态存储是整个程序执行期间都存在的存储方式,使变量成为静态的方式有两种: 一种是在函数外面定义它,另一种是在声明变量时使用关键字static。 static double fee = 56.50;
自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命,变量可能存在于程序的整个生命周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)。 -
动态存储 new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在C++中被陈为自由存储空间(free store)或堆(heap)。该内存池同用于静态变量和动态变量的内存是分开的。 数据的生命周期不完全受程序或函数内存时间控制,与使用常规变量相比,使用new和delete让程序员对程序如何使用内存有更大的控制权。 然而,内存管理也更复杂了。在栈中,自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更难。
注意:指针是功能最强大的C++工具之一,但也最危险,因为它们允许执行对计算机不友好的操作,如:使用未经初始化的指针来访问内存或者试图释放同一个内存块两次。
类型组合
可以各种方式组合数组、结构和指针。
struct antaractica_years_end
{
int year;
};
//创建该类型的变量
antaractica_years_end s01,s02,s03; //s01,s02,s03 are structures
//使用成员运算符访问其成员
s01.year = 1998;
//创建指向该结构的指针
antaractica_years_end * pa = &s02;
//使用间接成员运算符来来访问成员
pa->year = 1999;
//创建结构数组
antaractica_years_end trio[3];
//使用成员运算符访问元素的成员
trio[0].year = 2004; // trio[0] is a structure
//其中trio是一个数组,trio[0]是一个结构,而trio[0].year是该结构的一个成员。
//由于数组名是一个指针,也可以使用间接成员运算符
(trio+1)->year = 2006; // same as trio[1].year = 2006;
//可创建指针数组
const antaractica_years_end * arp[3] = {&s01,&s02,&s03};
//因为arp是一个指针数组,arp[1]就是一个指针,可将间接成员运算符应用于它,以访问成员:
std::cout << arp[1]->year << std::endl;
//可创建指向上述数组的指针
const antaractica_years_end ** ppa = arp;
//其中arp是一个数组的名称,因此它是第一个元素的地址。
//但其第一个元素为指针。因此ppa是一个指针,指向antaractica_years_end的指针
//C++11版本提供了auto,编译器知道arp的类型,能够正确地推断出ppb的类型
auto ppb = arp; // C++ automatic type deduction
//由于ppa是一个指向结构指针的指针,因此*ppa是一个结构指针,可将间接成员运算符应用于它
std::cout << (*ppa)->year << std::endl;
std::cout << (*(ppb+1))->year << std::endl;
//由于ppa指向arp的第一个元素,因此*ppa为第一个元素,即&s01。
//(*ppa)->year为s01的year成员。
//ppb+1指向下一个元素arp[1],即&s02.
程序清单:
#include <iostream>
using namespace std;
struct antaractica_years_end
{
int year;
};
int main()
{
antaractica_years_end s01,s02,s03;
s01.year = 1998;
antaractica_years_end * pa = &s02;
pa->year = 1999;
antaractica_years_end trio[3];
trio[0].year = 2004;
std::cout << trio->year << std::endl;
const antaractica_years_end * arp[3] = {&s01,&s02,&s03};
std::cout << arp[1]->year << std::endl;
const antaractica_years_end ** ppa = arp;
auto ppb = arp; // C++ automatic type deduction
std::cout << (*ppa)->year << std::endl;
std::cout << (*(ppb+1))->year << std::endl;
return 0;
}
运行结果:
2004
1999
1998
1999
数组的替代品
模板类vector和array是数组的替代品。
1、模板类vector
模板类vector类似于string类,也是一种动态数组。可以在运行阶段设置vector对象的长度,可在末尾附加新数据,还可以在中间插入新数据。基本上,它是使用new创建动态数组的替代品。
实际上,vector类确实使用new和delete来管理内存,但这种工作是自动完成的。
首先,要使用vector对象,必须包含头文件vector。其次,vector包含在名称空间std中,因此可以使用using编译指令、using声明或std::vector。第三,模板使用不同的语法来指出它存储的数据类型。第四,vector类使用不同的语法来指定元素数。
#include <vector>
using namespace std;
vector<int> vi; // create a zero-size array of int
int n;
cin >> n;
vector<double> vd(n); // create an array of n doubles
其中,vi 是一个vector 对象,vd 是一个vector对象。由于vector对象在插入或添加值时自动调整长度,因此可以将vi的初始长度设置为零。但要调整长度,需要使用vector包中的各种方法。
一般而言,声明创建一个名为vt的vector对象,它可存储n_elem个类型为typeName的元素:
vector<typeName> vt(n_elem);
其中参数n_elem可以是整型常量,也可以是整型变量。
2、模板类array(C++11)
vector类的功能比数组强大,但付出的代价是效率稍低。如果需要的是长度固定的数组,使用数组是更佳的选择,但代价是不那么方便和安全。
有鉴于此,C++11新增了模板类array,它也位于名称空间std中,与数组一样,array对象的长度也是固定的,也使用栈(静态内存分配),而不是自由存储区,因此效率与数组相同,但更方便,更安全。
要创建array对象,需要包含头文件array。array对象的创建:
#include <array>
...
using namespace std;
array<int,5> ai; // create array object of 5 ints
array<double,4> ad = {1.2,2.1,3.43,4.3};
推而广之,声明创建一个名为arr的array对象,它包含n_elem个类型为typename的元素;
array<typeName,n_elem> arr;
与创建vector对象不同的是,n_elem不能是变量。
在C++11中,可将列表初始化用于vector和array对象,但在C++98中,不能对vector对象这样做。
3、比较数组、vector对象和array对象
#include <iostream>
#include <vector> //STL C++98
#include <array> // C++11
using namespace std;
int main()
{
double a1[4] = {1.2,2.4,3.6,4.8};
vector<double> a2(4); // create vector with 4 elements
a2[0] = 1.0 /3.0;
a2[1] = 1.0 /5.0;
a2[2] = 1.0 /7.0;
a2[3] = 1.0 /9.0;
array<double,4> a3 = {3.14,2.72,1.62,1.41};
array<double,4> a4;
a4 = a3;
// use array notation
cout << "a1[2]: " << a1[2] << " at " << &a1[2] << endl;
cout << "a2[2]: " << a2[2] << " at " << &a2[2] << endl;
cout << "a3[2]: " << a3[2] << " at " << &a3[2] << endl;
cout << "a4[2]: " << a4[2] << " at " << &a4[2] << endl;
// misdeed
a1[-2] = 20.2;
cout << "a1[-2]: " << a1[-2] << " at " << &a1[-2] << endl;
cout << "a3[2]: " << a3[2] << " at " << &a3[2] << endl;
cout << "a4[2]: " << a4[2] << " at " << &a4[2] << endl;
return 0;
}
运行结果:
a1[2]: 3.6 at 0x61fdf0
a2[2]: 0.142857 at 0x771880
a3[2]: 1.62 at 0x61fdb0
a4[2]: 1.62 at 0x61fd90
a1[-2]: 20.2 at 0x61fdd0
a3[2]: 1.62 at 0x61fdb0
a4[2]: 1.62 at 0x61fd90
程序说明:
首先,注意到无论数组、vector对象还是array对象,都可以使用标准数组表示法来访问各个元素。其次,从地址可知,array对象和数组存储在相同的内存区域(栈)中,而vector对象存储在另一个区域(自由存储区域或堆)中。第三,注意到可以将一个array对象赋给另一个array对象;而对于数组,必须逐元素复制数据。
这行代码需要特别注意:
a1[-2] = 20.2; // 转换为 *(a1 - 2) = 20.2;
其含义:找到a1指向的地方,向前移动两个double元素,并将20.2存储到目的地。也就是,将信息存储到数组的外面。与C语言一样,C++也不检查这种超界错误。
使用成员函数begin()和end()能够确定边界,以免无意间超界。
|