C++初探 3-2(处理数据)

C++初探 3-2(处理数据),第1张

目录

注:

const限定符

浮点数

书写浮点数

浮点类型

浮点常量

浮点数的优缺点

浮点数的在内存中的存储

C++算数运算符

基本运算

 运算符优先级和结合性

除法分支(/)

求模运算符(%)

类型转换

1. 初始化和赋值进行的转换

2. 以{}方式初始化时进行的转换(C++11)

3. 表达式中的转换

4. 传递参数时的转换

5. 强制类型转换

运算符集合表

C++11中的auto声明


注:

        本笔记参考:《C++ PRIMER PLUS(第6版)》


const限定符
const type_name = value;    //创建常量的通用格式

        常量的符号名称:符号名称指出了常量表示的内容。

#define ERROR 0

        像上述这种使用 #define语句 的方法,可以应对程序中多个地方使用同一个常量的情况:当需要修改常量时,只需修改该符号定义即可。

        但C++有一种更好的处理符号常量的方法,即使用 const关键字 修改变量声明和初始化。

例子:

const int Months = 12;    //此处的 Months 就是 12 的符号常量

        当常量(如上述的Months)被初始化后,其值被固定,编译器将不再允许修改该常量的值。

一些建议:

  • 常量(类似于 Months),将名称的首字母大写;
  • 使用#define创建的常量,将整个名称大写。

错误的示范:

const int toes;
toes = 10;

        这种写法不仅会让变量的值不确定,且无法修改。

const的优点

  1. const能够明确指定类型;
  2. 可以使用C++的 作用域规则 将定义限制在特定的函数或文件中;
  3. 可以将const用于更复杂的类型,如数组和结构。

        在C++中,const值可以被用来声明数组长度。

浮点数

        浮点数——能够表示小数部分的数字。它们提供的值的范围将会更大。

        在计算机中,浮点数被分为两部分进行存储:

  • 部分1:表示值;
  • 部分2:用来对值进行放大和缩小。
书写浮点数

        C++提供了两种书写浮点数的方法。

  • 第一种是使用常用的标准小数点表示法:
12.34
9390001.32
0.00032
8.0            //即使小数部分是0,小数点也将确保数字以浮点数的格式表示
  • 第二种是E表示法:

        例如:

        除此之外,还有一些例子:

2.52e+8    //使用 E 或者 e 都是可以的
8.33E-4    //指数部分(如:-4)可以是负数,也可以省略符号
7E5          //和 7.0E + 05 表达的意思是一致的
-18.32e13    //数字开头可加正负号

        在上述代码中,我们可以看到类似于 2.52e+8、8.33E-4 的书写方式。其中:

  • xxxxE+n 指的是小数点向右移动 n 位;
  • xxxxE-n 指的是小数点向左移动 n 位。

        E表示法的特点使其最适合描述非常大和非常小的数。上述代码展示了一些书写规范,注意:数字中是不能有空格的。

int main()
{
	float f = -8.33E2;
	return 0;
}

        上述代码中的-8.33E2指的是-833,要注意,E表示法中,前面的符号用于数值,指数的符号用于缩放。

浮点类型

        有效位(significant figure):是数字中有意义的位。

        类似于ANSI C,C++也有3种浮点类型:float、double 和 long double。这些类型可以按它们可以表示的有效位数允许的指数的最小范围来描述。

        C和C++对于有效位数的要求是:

  • float至少是32位;
  • double至少是48位,且不少于float;
  • long double至少和double一样多。

(这意味着这三种类型的有效位数可以一样多。而通常地,float为32位,double为64位,long double为80、96或128位。)

        另外,这3种类型的指数范围至少是-37到37。

        可参考头文件 cfloat 或者 float.h。

#include

int main()
{
	using namespace std;

	cout.setf(ios_base::fixed, ios_base::floatfield);	//使用了ostream方法setf()
	float A_a = 10.0 / 3.0;
	double B_b = 10.0 / 3.0;
	const float million = 1.0e6;

	cout << "A_a = " << A_a;
	cout << ",million * A_a = " << million * A_a << ",";
	cout << "\n10 * million * A_a = " << 10 * million * A_a << "\n\n";

	cout << "B_b = " << B_b;
	cout << ",million * B_b = " << million * B_b << "\n";
	return 0;
}

程序执行的结果是:

分析:

        在上述代码中使用了ostream方法setf()。这种调用迫使输出使用顶点表示法,使程序显示到小数点后6位,可以更好地了解精度,并且防止程序把较大的值切换成E表示法。

        在上述代码中,A_a(float类型) 和 B_b(double类型) 都被初始化成 10.0 / 3.0 = 3.333……,此时,由于cout只打印6位小数,所以 A_a 和 B_b 的值是同样精准的。但是在乘以一百万后,精确值就发生了改变:A_a 有7位有效位是精确的,而 B_b 至少有13位是精确的。

(ps:由于系统只确保double有15位有效位,所以将B_b乘一千万后,得到的结果就不精确了。)

浮点常量

        在从程序中书写浮点常量时,默认情况下,浮点常量都属于double类型。

  • 如果要使常量为float类型,可以使用 f 或者 F 后缀;
  • 如果要使用long double类型,可以使用 l 或者 L 后缀。

        一些例子:

234f        //float类型
45E20F      //float类型
323E23      //double类型
2.2L        //long double类型

浮点数的优缺点

        与整数相比,浮点数有两个优点:

  1. 它们可以表示整数之间的值;
  2. 由于缩放因子,它们可以表示的范围大得多。

        与之相对,也有缺点:

  • 浮点数的运算速度要慢于整数运算,且精度会降低。
#include

int main()
{
	using namespace std;
	float a = 2.34E+22f;
	float b = a + 1.0f;

	cout << "a     = " << a << endl;
	cout << "b - a = " << b - a << endl;
	return 0;
}

程序执行结果:

分析:

        注意,在上述的代码中,我们将 变量a 的数字+1(float b = a + 1.0f;),按理说,此时 b - a 应该等于 1 ,但实际上的结果却是 0 。

        之所以出现这样的情况,是因为float类型只能表示数字中的前6位前7位,但是2.34E+22却是一个过大的数字:

        注意 1 所在的位置,它已经超过了float类型会表示的范围。

浮点数的在内存中的存储

        浮点数在内存中的存储方式和整型截然不同,二者之间如果强制转换,容易发生错误:

#include
int main()
{
	using namespace std;
	int n_Int = 9;
	float* f_Float = (float*)&n_Int;    //通过强制类型转换,强行将int类型的值赋给float变量
	
	cout << "n_Int\t= " << n_Int << "\nf_Float = " << *f_Float << endl << endl;

	*f_Float = 9.0;						//将float变量的值改为 9.0
	cout << "n_Int\t= " << n_Int << "\nf_Float = " << *f_Float << endl << endl;
	return 0;
}

程序执行的结果是:

        注:强制类型转换是不会改变变量的值的,发生改变的是变量的地址类型

        从中我们可以得到一个结论:浮点型和整型在内存中的存储方式(和解读方式)是有区别的

------

        接下来将解释浮点数在内存中的存储。

        根据国际标准IEEE(电气和电子工程协会)754,任意一个 二进制浮点数 可以表示成下面的形式:

        浮点数十进制转换为上述的二进制形式时,要根据小数点的存在,往前的 整数部分 和往后的 小数部分 分开判断。例如:

IEEE 754规定:

        1.对于32位的浮点数,最高位的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。

        2.对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

 IEEE 754对有效数字M的特别规定

         注意:1 <= M < 2

        之前也提到过,当一个数被转换成二进制形式并且使用科学计数法进行记录时(如上图中的5.5),其有效数字M可以被写成 1.xxxx 的形式(如上图中的 1.011 * 2²),从中我们可以得出:M的整数部分总是为 1

        利用上述的结论:因为整数部分的 1 总是存在的,这也意味着在存储时,这个 1 可以被省略,在存储时,我们只需存储小数部分(即 1.xxxx 中的 xxxx),等到读取时在把这个 1 补上。这样的存储规则使得浮点数在内存存储中多了一位有效数字。

 IEEE 754对指数E的特别规定

        指数E被规定为一个 unsigned int 类型的数据。这规定了E的取值范围:

  • 如果E为8位,它的取值范围为0~255;
  • 如果E为11位,它的取值范围为0~2047。

        但是,科学计数法中的E是可以出现负数的(如上图例子中的0.5)。为了处理这种情况,IEEE 754 规定,存入内存E的真实值必须再加上一个中间数(得到计算值):

  • 对于8位的E,这个中间数是127(就是E所在的8bit存储空间中除了最高位,全部变为1);
  • 对于11位的E,这个中间数是1023(类似于上面)。

        除此之外,浮点数从内存中取出还可以分成三种情况:

情况对应表示规则
E不全为0或不全为1指数E的的计算值减去127(或1023),得到真实值,再在有效数字M前加上第一位的1。(正常运算)
E全为0

这说明该浮点数接近0

此时,浮点数的指数E等于1~127(或者1~1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。

E全为1

这说明该浮点数接近无穷

此时,如果有效数字M全为0,表示±无穷大(正负取决于符号位S)。

C++算数运算符 基本运算

         C++提供了几种运算符来完成5种基本的运算:

  • +运算符 —— 对 *** 作数执行加法运算。
  • -运算符 —— 从第一个 *** 作数中减去第二个 *** 作数。
  • *运算符 —— 将 *** 作数相乘。
  • /运算符 ——用第一个数除以第二个数。得到的商只会是商的整数部分。
  • %运算符 —— 进行求模运算(取余)。(两个 *** 作数必须是整型,如果其中一个 *** 作数是负数,则结果的符号满足:(a/b)*b + a%b = a。)

例如:

#include

int main()
{
	using namespace std;
	float A_a = 0;
	float B_b = 0;

	cout.setf(ios_base::fixed, ios_base::floatfield);	//强制显示小数点后六位
	cout << "请输入A_a对应的数字: ";
	cin >> A_a;
	cout << "请输入b对应的数字: ";
	cin >> B_b;

	cout << "\nA_a = " << A_a << ",B_b = " << B_b << endl;
	cout << "A_a + B_b = " << A_a + B_b << endl;
	cout << "A_a - B_b = " << A_a - B_b << endl;
	cout << "A_a * B_b = " << A_a * B_b << endl;
	cout << "A_a / B_b = " << A_a / B_b << endl;
	return 0;
}

出现执行的结果是:

分析:

        在上述的程序中存在:11.17 + 50.25 = 61.419998 。这和结果是冲突的。发生这种情况的原因是float类型表示有效位数的能力有限(对于float类型,C++只保证6为有效位,即61.4200)。

 运算符优先级和结合性
  1. 当一个表达式中存在多个运算符时,C++会根据优先级规则来决定首先使用哪个运算符。
  2. 当运算符的优先级相同时,C++将会看 *** 作数的结合性是从左到右,还是从右向左。

        上述两项规则在上表中均有体现。但是,上述规则之外的情况依旧是存在的:

int dues = 20 * 5 + 24 * 6;

        在这条语句中,无论是优先级还是结合性都没有规定先执行哪个乘法。实际上,这个问题被C++留给了实现。(虽然上述语句无论使用哪种顺序结果都是一样的,但还是会出现结果不一样的情况。)

        建议:在不熟悉运算符的时候,尽量多使用 () ,即能保证运算顺序和预想的一致,也方便观察。

除法分支(/)

        除法 *** 作符(/)的行为取决于 *** 作符的类型:

两个 *** 作符均为整数

        此时C++将执行整数除法:舍弃结果的小数部分,使得最后的结果是一个整数。

两个 *** 作符中至少存在一个浮点数

        此时将保留小数部分,使结果是一个浮点数。

例子:

#include

int main()
{
	using namespace std;
	
	cout.setf(ios_base::fixed, ios_base::floatfield);
	cout << "整数除法  :9/5     = " << 9 / 5 << endl;			//舍弃小数位数
	cout << "浮点数除法:9.0/5.0 = " << 9.0 / 5.0 << endl;
	cout << "混合型除法:9.0/5   = " << 9.0 / 5 << endl;
	cout << endl;
	cout << "双精度浮点型:1e7/9.0     = " << 1e7 / 9.0 << endl;
	cout << "单精度浮点型:1e7f/9.0f   = " << 1e7f / 9.0f << endl;
	return 0;
}

程序执行的结果是:

分析:

        对于:

	cout << "混合型除法:9.0/5   = " << 9.0 / 5 << endl;

这种混合型除法,在进行运算时,C++将会把它们转换成同一类型,这就是自动转换

---

        另一方面,对于:

	cout << "双精度浮点型:1e7/9.0     = " << 1e7 / 9.0 << endl;
	cout << "单精度浮点型:1e7f/9.0f   = " << 1e7f / 9.0f << endl;

当两个 *** 作数都是double类型时,结果也是double类型,float类型同理。

注:浮点常量在默认情况下为double类型。

运算符重载

        C++根据上下文(在上述程序中指 *** 作数的类型)来确定运算符的定义。使用相同的独好进行多种 *** 作被称为运算符重载

        在上述程序中,除法运算符表示了3种不同的运算:int除法、float除法和double除法。这就是运算符重载的一个例子。除此之外,C++还允许扩展运算符重载。

求模运算符(%)

        求模运算符返回的是除法的余数

例如(磅转换成英石,并且余下多少磅):

#include

int main()
{
	using namespace std;
	const int con = 14;
	int pounds = 0;

	cout << "请输入重量(单位:磅): ";
	cin >> pounds;
	int stone = pounds / con;
	int remain = pounds % con;
	cout << pounds << "磅 = " << stone << "石余" << remain << "磅\n";

	return 0;
}

程序执行的结果是:

类型转换

        C++有11种整型和3种浮点类型,这就导致计算机需要处理大量不同的情况,尤其是进行不同类型数据的计算时。为了处理这种状况,C++将在下面几种情况时对值进行类型转换:

  • 将一种算数类型的值赋给另一种算数类型的变量时;
  • 表达式中包含不同的类型时;
  • 将参数传递给函数时。

        下面将介绍这些自动转换的规则。

1. 初始化和赋值进行的转换

        C++允许将一种类型的值赋给另一种类型的变量。此时,值将会被转换为接收变量的类型

例如:

#include

int main()
{
	using namespace std;
	short thi = 120;
	long fir = thi;

	return 0;
}

        这串代码将 short类型 的变量thi赋给了 long类型 的变量fir,可以发现:

thi是16位,而fir是32位。当thi内的数据被赋给fir时,thi的值会被扩展成32位,这个值被存储在fir内,而thi内的值保持不变。

        像上述这种将一个值赋给取值范围更大的类型通常是不会出现问题的,但是仍然会存在出现问题的情况:

被转换的类型目标类型例子潜在问题
较大的浮点类型较小的浮点类型double转换成float精度(有效数位)降低,值可能超过目标类型的取值范围,这种情况下,结果不确定。
浮点类型整型float转换成int小数部分丢失,原本的值可能超出目标类型的取值范围,这种情况下,结果不确定。
较大的整型较小的整型long转换成short原来的值可能超出目标类型的取值范围,通常只复制右边的字节

        除此之外,将0赋给bool变量时,将被转换成false,非零值将被转换成true。

接下来将演示一些例子:

#include

int main()
{
	using namespace std;

	cout.setf(ios_base::fixed, ios_base::floatfield);
	float A_a = 3;			//int类型转换成float类型
	int B_b(3.9832);		//double类型转换成int类型
	int C_c = 7.2E12;		//C_c的结果是未定义的
	cout << "A_a = " << A_a << endl;
	cout << "B_b = " << B_b << endl;
	cout << "C_c = " << C_c << endl;
}

程序执行的结果是:

分析:

        B_bC_c 都是浮点类型转换成整型,此时,C++截取原本数字的整数部分,小数部分被丢弃,因此导致B_b和C_c的值和原本的值产生了偏差。

        对于C_c,由于int类型的变量是无法存储7.2E12的,这就是C++没有定义的情况了。


2. 以{}方式初始化时进行的转换(C++11)

        在C++11中,存在一种使用大括号的初始化——列表初始化。这种初始化常用于给复杂的数据类型提供值列表。

        比起其他初始化方式,列表初始化要来得更加严格:列表初始化不允许缩窄,即不允许不同变量类型之间的转换,但是像不同整型之间的转换之类的情况可能可以被允许,例如:int变量初始化为long值(前提是int变量足够大,或者这个long值够小)

例如:

#define y 31325
int main()
{
	//被允许的情况
	const int code = 66;
	int x = 66;
	char c1 = { 66 };
	char c2{ code };
	char c3 = y;			//会发生截断
	char c4{ 31325 };		//char类型所占存储空间过小,无法存储,需要收缩转换

	//不被允许的情况
	char c5 = { x };		//x不是一个常量

    return 0;
}

3. 表达式中的转换

        当同一个表达式中包含两种不同的算数类型时,C++有两种自动转换可以选择:

  1. 一些类型在出现时就会自动转换;
  2. 不同类型同时出现在表达式中时将被转换。

        此处着重表达式中的自动转换。

相同数据类型(以整型为例)

        在计算表达式时,C++会将短于 int类型 的整型类型转换成 int类型(包括boo l、char 、unsigned char 、signed char 和 short)。这些转换被称为整型提升:

int main()
{
	short a = 20;
	short b = 30;
	short c = a + b;
	return 0;
}

        在 short c = a + b; 的计算过程中,a 和 b 将被转换成 int类型 进行计算,在得出结果后,程序会把结果再转换成 short类型 。

        通常,int类型被选择为计算机最自然的类型,这意味着计算机使用这种类型时,运算速度可以是最快的。

        除此之外,还存在一些整型提升:

不同数据类型之间

        当运算涉及两种不同的类型时,较小的类型将被转换成较大的类型。编译器通过校验表确定在算数表达式中执行的转换,此处校验表是C++11版本的:

        传统K&R C的规则与ANSI C稍有不同,例如:传统C语言总是将float提升为double,即使两个 *** 作数均为float类型。

有符号整型的级别由高到低排列:

  1. long long
  2. long
  3. int
  4. short
  5. signed char

无符号整型的级别由高到低排列:

  1. unsigned long long
  2. usigned long
  3. usigned int
  4. unsigned short
  5. unsigned char

        其中:

  • char 、signed char 和 usigned char 级别相同;
  • bool类型的级别最低;
  • wchar_t 、char16_t 和 char32_t 的级别与其底层类型相同。

4. 传递参数时的转换

        传递参数时的类型转换通常由C++函数原型控制。但是这种控制是可以取消的,此时C++将:

  • 对char和short类型(有无符号都是)应用整型提升;
  • 将float参数提升为double。

5. 强制类型转换

        C++允许通过强制类型转换进行显式的类型转换。强制类型转换有两种格式:

(typeName)value;    //继承自C语言
typeName(value);    //纯粹的C++

//例如:
(long)thorn;
long(thorn);
//强制类型转换不会修改thorn变量本身,而是创建一个新的值,这个值可以在表达式中被使用。

        C++的开发者认为C语言的强制类型转换存在过多的可能性,这可能带来巨大的风险。因此,在C++中我们可以见到4个强制类型转换运算符,在下面运算符的集合表中有这4个运算符。

        接下来将简单介绍其中一个其中类型转换运算符 static_cast

static_cast (value);

使用例:

int main()
{
	short b = 30;
	static_cast (b);
	return 0;
}

例子:

#include

int main()
{
	using namespace std;
	int A_a, B_b, C_c;

	A_a = 19.99 + 11.99;        //计算机运算并不存在四舍五入,此处是先以浮点数的形式计算出结果,再将结果进行强制类型转换,赋给A_a。
	B_b = (int)19.99 + (int)11.99;
	C_c = int(19.99) + int(11.99);
	cout << "A_a = " << A_a << endl;
	cout << "B_b = " << B_b << endl;
	cout << "C_c = " << C_c << endl;
	cout << endl;

	char ch = 'Z';
	cout << ch << " 的编码是 " << int(ch) << endl;
	cout << ch << " 的编码就是 " << static_cast(ch) << endl;
	return 0;
}

程序执行的结果是:

分析:

        这个程序体现了强制类型转换的两个原因:

  • 可能有一些值被存储为一个类型,但是需要使用它们来计算得出的值是另一个类型的。
    A_a = 19.99 + 11.99;
    B_b = (int)19.99 + (int)11.99;
    C_c = int(19.99) + int(11.99);
  • 使一种格式的数据能够满足不同的期望。
    cout << ch << " 的编码是 " << int(ch) << endl;
    cout << ch << " 的编码就是 " << static_cast(ch) << endl;

运算符集合表
优先级运算符结合性含义
第1组::作用域解析运算符
第2组(表达式)分组
()L-R函数调用
()值构造,即type(expr)
[ ]数组下标
->间接成员运算符
.直接成员运算符
const_cast专用的类型转换
dynamic_cast专用的类型转换
reinterpret_cast专用的类型转换
static_cast专用的类型转换
typeid类型标识
++加1运算符,后缀
--减1运算符,后缀
第3组!逻辑非
~位非
+一元加号(正号)
-一元减号(负号)
++加1运算符,前缀
--减1运算符,后缀
&地址
*解除引用(间接值)
()类型转换,即(type)expr
sizeof求长度,以字节为单位
new动态分配内存
new[ ]动态分配数组
delete动态释放内存
delete[ ]动态释放数组
第4组.*L-R成员解除引用
->*间接成员解除引用
第5组*L-R
/
^模(余数)
第6组+L-R
-
第7组<<L-R左移
>>右移
第8组<L-R小于
<=小于等于
>=大于等于
>大于
第9组==L-R等于
!=不等于
第10组&L-R按位AND
第11组^L-R按位XOF(异或)
第12组|L-R按位或
第13组&&L-R按位与
第14组||L-R按位或
第15组: ?R-L条件(三目)
第16组=R-L简单赋值
*=乘并赋值
/=除并赋值
%=求模并赋值
+=加并赋值
-=减并赋值
&=按位与并赋值
^=按位异或并赋值
|=按位或并赋值
<<=左移并赋值
>>=右移并赋值
第17组throwL-R引发异常
第18组,L-R合并两个表达式

C++11中的auto声明

||| 关键字auto:让编译器根据初始值的类型推断变量的类型。

        在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同:

auto n = 100;        //n是int类型
auto x = 1.5;        //x是double类型
auto y = 1.3e12L;    //y是long double类型

        但是自动推断类型在处理简单情形时,可能会出问题。假设要将变量x、y和z都指定为double类型,那么:

auto x = 0.0;       //初始化为double类型
double y = 0;       //初始化为double类型
auto z = 0;         //此处z会被初始化成int类型,因为程序会认为0是int类型的变量

        而在处理复杂类型时,例如要使用标准模块库(STL)中的类型:

std::vector scores;
std::vector::iterator pv = scores.begin();

此时,C++11允许如下这种写法:

std::vector scores;
auto pv = scores.begin();

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/3002191.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-27
下一篇 2022-09-27

发表评论

登录后才能评论

评论列表(0条)

保存