Professional C++第四版 中文

第一部分

Professional C++ 介绍

第一章:C++和标准库速读

第二章:Strings和String Views的使用

第三章:编码与风格

 

第一章:C++和标准库速读

本章内容

-------------------------------------------------------------------------------------------
➤➤ 简要浏览C++语言和标准库中最重要的部分和语法
➤➤ 智能指针基础
在WROX.COM上下载本章 

-------------------------------------------------------------------------------------------
请注意,本章所有的代码示例都可以作为章节的一部分在本书网站( www.wrox.com/go/proc++4e)的下载页上下载。
本章的目的是通过简要讲解C++中最重要的部分使你掌握基本的知识,以便继续后面的学习。本章不是一节C++编程语言或者标准库的综合性课程。一些基础知识点不会介绍,如什么是编程,什么是递归。像怎样定义一个union或者volatile关键字这样的高级知识点也略过。一些与C++关联不大的C语言部分将在后续章节作为C++的一部分深入讲解。

本章主要介绍C++中程序员每天会遇到的部分。例如,如果你有一段时间没有用C++了,并且记了for循环的语法,你可以在本章中找到它。如果你是个C++新手,不懂什么是引用变量,那么你也可以在这里学习到它。你也能学习到怎样使用标准库所支持功能的基础技能,如vector容器,string类和智能指针。

如果你使用C++有非常丰富的经验,不需要再回顾语言中任何基础部分,跳过这一章。如果你对C++陌生就细心阅读本章,并确保你已经理解了例子。如果你还需要补充一些内容,请参考附录B中罗列的文章。

C++基础
        C++语言经常被看作“better C”或者C的超集(“supeset of C”)。它主要是按照面向对象的C来设计的,通常被叫作“带类的C”。后来,C语言中很多令人烦恼和不完善的地方都被很好的处理。因为C++是基于C的,如果你是一个有经验的C程序员,本章中你所看到的大部分C++语法都很熟习。这两种语言确实有一些不同之处。一个明显的证据是:C++创始人Bjarne Stroustrup写的C++编程语法书(第四版, Addison-Wesley Professional, 2013)有1368页,而Kernighan和Ritchie两人的C编程语法(第二版,Prentice Hall, 1988)只有274页。所以,如果你是一个C编程员,找出新的或者不熟习的语法吧!
        Hello, World尊享其荣,下面是一个你可能遇见过最简单的C++程序:

// helloworld.cpp
#include <iostream>
int main()
{
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

正如你所想,这段代码在屏幕上打印出“Hello, Wrold!”消息。这是一个简单的程序,仅此而已。但是它确实展现了C++程序格式中如下重要的概念:
       ➤➤ 注释
       ➤➤ 预处器指令
       ➤➤ main()函数
      ➤➤ I/O 流

将在下面的篇幅中简要阐释这些概念。
       注释
程序的第一行是注释。它只是为程序员而存在的一条信息,编译器会忽略它。在C++中有两种添加注释的方法,前面和下面这个例子,两个斜杠标识这一行的后面所有内容都是是注释。

// helloworld.cpp

使用多行注释也能达到同样的效果,即被编译器忽略。多行注释以/*开始,以*/结束。下面的代码展示了一个有效的多行注释(或者,更确切一些,非有效【1】)。

/* This is a multiline comment.
The compiler will ignore it.
*/

在第三章会详细的介绍注释。

预处理器指令
构建一个C++程序需要三步。第一,预处理器处理代码,这一步找出代码中的元信息;接着,代码被编译或者翻译到能被机器可读的目标文件中;最后,将各个目标文件联接到一起组成一个单独的应用程序。
预处理器的指令以#号开始,如前面例子中的 #include <iostream>。此例中,#include指令告诉预处理器将 <iostream> 头文件中的所有内容都拿过来并对当前文件可用。头文件最常见的作用是声名函数,这些函数将在其它地方定义。函数声明告诉编译器如何调用函数,声明形参的数量和类型,以及函数的返回类型。函数定义包含了函数真正的代码。C++中声名通常放在以.h结尾的头文件中,定义通过放在以.cpp结尾的源文件中。其它大多数编程语言不会把声名和定义分开放在不同的文件中,如C#、java。头文件<iostream>声名C++支持的输入输出机制。如果程序不包含这个头文件,那它唯一的输出文本功能就无法完成。

注: 在C中标准库头文件的名字通常以.h结尾,如<stdio.h>,且不使用命名空间。在C++中,标准库头文件摒弃了.h后缀,如<iostream>,所有的东西都定义在std命名空间或者它的一个子命名空间中。

C++中的依然有C标准库头文件,但它们有两种版本:
       ➤➤ 新的和推荐版本不带.h后缀,但有一个c前缀。这些版本把所有的东西都放在std命名空间中
       ➤➤ 旧版本带.h后缀。这些版本不使用命名空间(如<stdio.h>)

下面的表列举了最常用的预处理器指令。

预处理器指令功能常见用法
#include [file]在指令的位置插入指定的文件。它通常用来包含头文件,这样代码就可以使用其它地方定义的功能。
#define [key]
[value]

在指定key的地方用指定的value替换。

 

C中常用来定义常量或宏。C++中为常量和大多数类型的宏提供了更好的机制。宏不安全必有小心使用它们。详细内容参见11章。
#ifdef [key]
#endif
#ifndef [key]
#endif
根据是否用 #define定义了key来决定ifdef(“if defined”)或者ifndef("if not defined")块包含还是丢弃。被频繁的用来防止循环包含。每一个头文件都以#ifndef开始,检测一个未被定义的key,接下来#define指令定义这个key。头文件以#endif结束。这可以防止文件被多次定义;看此表后面的例子。
#pragma [xyz]xyz 依赖于编译器。在预处理过程中如果到达指令时,它通常允许程序员显示一条警告或者错误。看此表后面的例子。

        使用预处理器指令来避免多重包含的例子如下:

#ifndef MYHEADER_H
#define MYHEADER_H
// ... the contents of this header file
#endif

        如果你的编译器支持 #pragma once指令,可以像下面这样书写。大多数现代编译器都支持。

#pragma once
// ... the contents of this header file


      第11章将深入介绍这个。

       main()函数
       当然,main()是程序的入口点。 main() 的返回类型是int,指示程序的结果状态。你可以显示忽略main()的return语句,这种情况下,0会自动被返回。main()函数要么没有参数,要么像下面这样有两个参数。

int main(int argc, char* argv[])

argc指示输入程序的参数个数,argv包含这些参数。注意,argv[0]可以是程序的名字,但它也有可能是一个空字符串,所以不要依赖于它。相反,使用平台关联的功能来获取程序名字。需要记住的是实际参数是从索引号1开始的。
 

      I/O 流
I/O 流将在13章深入讲解,但是基本的输入输出是非常简单的。输出流就像一个数据的洗衣槽,你放进去的任何东西它都能适当的输出。 std::cout 是对应控制台或者标准输出的槽。还有其它的槽,如std::cerr是向错误控制台输出的槽。<<操作符将数据抛给槽。在前面的例子中,用引号引住的字符串被输出到标准输出上。可以在一行上将多种类型的数据依次的发送到输出流上。下面的代码依次的将字符串、数值、字符串输出:

std::cout << "There are " << 219 << " ways I love you." << std::endl;

         std::endl代表一行结束。当输出流遇到std::endl时,它将目前为止发送到槽中的所有数据输出并移动到下一行。也可以用\n表示一行结束。\n字符是一个转意序列,代表新行字符。可以在什么引号引住的字符串中使用转意序列。下表列出了一些最常见的转意序列:

 

\n新行
\r回车
\tTAB
\\反斜杠
\"引号

      流也可以用来接收用户输入。最简单的方法是使用输入流结合>>操作符来完成。std::cin输入流接收来自用户的按键。例子如下:

int value;
std::cin >> value;

        因为不知道用户会输入什么样的数据,所以用户输入会是一个棘手的问题。13章将就怎样使用输入流作一个全面的阐述。如果你是一个有C背景的C++新手,你可能会想使用以前经常使用的printf()和scanf()函数会怎样。在C++中这些函数依然可以使用,但我推荐使用流库,主要是因为prinft()和scanf()之类的函数不是类型安全的。

         命名空间
   命名空间解决了不同代码块之间名字冲突的问题。例如,你可能写一段代码包含了一个叫foo()的函数。某天,你决定使用一个第三方库,而这个库中也有一个叫foo()的函数。当你的代码中引用foo()时,编译器就没有办法知道它引用的是哪个版本的foo()。你无法改变库中的函数名,而且你也非常不想改你自己的。这种情况下命名空间就排上用场,因为你可以定义一个上下文,然后在此中定义名字。把代码包含到命名空间块内,就把它放在了命名空间中了。例如,下面的代码可以是namespaces.h文件的内容:

namespace mycode {
     void foo();
}

      方法或者是函数的实现也可以放在命名空间中。例如foo()函数,它在namespaces.cpp中的实现如下:

#include <iostream>
#include "namespaces.h"
void mycode::foo()
{
    std::cout << "foo() called in the mycode namespace" << std::endl;
}

     或者:

#include <iostream>
#include "namespaces.h"
namespace mycode {
    void foo()
    {
        std::cout << "foo() called in the mycode namespace" << std::endl;
    }
}

       通过把你的foo()放到"mycode"命名空间中,你的foo()就和第三方库中的分开了。为了调用命名空间版本的foo(),在函数名前通过::加上命名空间即可,::也叫作空间解释符。如下:

mycode::foo(); // Calls the "foo" function in the "mycode" namespace

      “mycode” 命名空间中的任何代码都可以调用本命名空间中的其它代码而不用加命名空间前缀。这种隐匿的命名空间对代码的可读性是非常有用的。使用using指令可以避免加命名空间前缀。using指令告诉编译器接下的代码使用指定的命名空间。这样就隐性的为代码使用命名空间,如下:

#include "namespaces.h"
using namespace mycode;

int main()
{
    foo(); // Implies mycode::foo();
    return 0;
}

        一个文件中可以多次使用using指令,但是注意不要过度使用这个捷径。极端的例子,如果你把你所有使用的命名空间都声名一下,那么就彻底摒弃了命名空间。如果你所使用的两个命名空间中有相同的名字,名字冲突问题就又出现了。知道自己的代码是在哪个命名空间中操作非常重要,它可以使用你避免意外的调用到错误版本函数。前面你已看过命名空间的语法——在Hello, World程序中使用命令空间,cout和endl确实是std命名空间中的定义的名字。使用using指令,你可以像下面这样写Hwllo, World:

#include <iostream>
using namespace std;
int main()
{
    cout << "Hello, World!" << endl;
    return 0;
}

        using声名可以用来引用命名空间中某个指定的项。 例如,你只打算使用命名空间中的cout,那么你可以使用using std::cout来引用它。后面的代码可以不用加命名空间前缀来引用cout,但是std命名空间中的其它项依然需要显示的加前缀来引用。

using std::cout;
cout << "Hello, World!" << std::endl;

        警告 永远不要在头文件中使用using指令或using声名;否则,引用你头文件的人就被迫的使用了它。

C++17

C++17使嵌套命名空间更易实现。嵌套命名空间中位于其它命名空间中的命名空间。在C++17之前,你需要像下面这样使用命名空间:

namespace MyLibraries {
    namespace Networking {
        namespace FTP {
            /* ... */
        }
    }
}

     C++17中你可以简单很多:

namespace MyLibraries::Networking::FTP {
    /* ... */
}

      名称空间别名可用于为另一个名称空间提供新的、可能更短的名称。例如 :namespace MyFTP = MyLibraries::Networking::FTP;

字面量
       在代码中使用字面量来书写数字和字符串。C++支持多种标准字面量。数字可以使用下面的字面量来指定(列表中的例子代表相同的数字,123):
➤➤ 十进制字面量, 123
➤➤ 八进制字面量, 0173
➤➤ 十六进制字面量, 0x7B
➤➤ 二进制字面量, 0b1111011
C++包含的其它字面量例子
➤➤ 浮点值(如3.14f)
➤➤ 双精度浮点值(如3.14)
➤➤ 字符(如'a')
➤➤ 以'\0'结束的字符数组(如"character array")
也可以定义自己的字面量,在11章中讲解这一高级的特性。在数值类型的字面量中可以使用数字分隔符。数字分隔符是一个单引号。例如,
➤➤ 23'456'789
➤➤ 0.123'456f

C++17
C++17增加了对十六进制浮点字面量的支持——如0x3.ABCp-10,0Xb.cp12l。

变量
在C++中,变量可以在任何地方声名,且在当前代码块声名该变量的行之后的任意地方使用它。变量在声名时可以不指定值。这些未初始化的变量通常是半随机的值,这取决于当时它所在内存的内容,也因此成为无数bug的根源。C++中的变量也可以在声名时赋初始值。下面的代码展示了这两种变量声名的样子,它们都使用代表整数值的int型。

int uninitializedInt;
int initializedInt = 7;
cout << uninitializedInt << " is a random value" << endl;
cout << initializedInt << " was assigned an initial value" << endl;

注意 大部分编译器都会对代码中的未初始化变量发出警告。有一些编译器会生成运行时报错的代码。

下表列出了C++中最常见的类型

类型描述用法
(signed) int
signed
正数和负数;数值范围依赖于编译器(通常4字节)int i = -7;
signed int i = -6;
signed i = -5;
(signed) short (int)短整数(通常2字节)short s = 13;
short int s = 14;
signed short s = 15;
signed short int s = 16;
(signed) long (int)长整数(通常4字节)long l = -7L;
(signed) long long (int)长长整数;数值范围取决于编译器,但是最小与长整数一样(通常8字节)long long ll = 14LL;
unsigned (int)
unsigned short (int)
unsigned long (int)
unsigned long long (int)
将上述类型的值限制在>=0unsigned int i = 2U;
unsigned j = 5U;
unsigned short s = 23U;
unsigned long l = 5400UL;
unsigned long long ll = 140ULL;
float浮点数float f = 7.2f;
double双精度浮点数,精度至少和浮点数一样double d = 7.2;
long double长双精确浮点数;精度至少和双精度浮点数一样long double d = 16.98L;
char单字符char ch = 'm';
char16_t16位单字符char16_t c16 = u'm';
char32_t32位单字符char32_t c32 = U'm';
wchar_t单宽度字符;宽度取决于编译器wchar_t w = L'm';
bool布尔型,其值为true和false二者之一bool b = true;
std::byte(需要包含<cstddef>头文件)单个字节。在C++17之前,用一个字符或者无符号字符来表示一个字节,但是这些类型用起来好像是你在处理字符。另一方面std::byte使你的意图更加明显——这是一个字节的内存std::byte b{42};(byte的初始化需要使用单个元素的直接列表初始化。参考本章后面的“直接列表初始化对比拷贝列表初始化”部分了解直接列表初始化的定义)

注意 C++不支持基本的字符串类型。但是,作为标准库的一部分标准库提供了一个标准的字符串实现。本章后面会介绍,深入讲解放在第二章中。

通过强制转换可以把变量改变成其它类型。如浮点型可以转换成整形。C++提供了三种显式的类型转换方法。第一种方法延续自C;不推荐使用但是很不幸它依然被广泛的使用。第二种方法很少使用。第三种方法是最详细但也是最清楚的,所以推荐使用。

float myFloat = 3.14f;
int i1 = (int)myFloat; // method 1
int i2 = int(myFloat); // method 2
int i3 = static_cast<int>(myFloat); // method 3

转换后的整数是浮点数去掉小数部分的值。第11章详细介绍这些转换方法的不同。在一些语境中,变量可以自动强制转换。如short可以自动地转换成long,因为long至少以相同的精度表示相同类型的数据

long someLong = someShort; // no explicit cast needed

当变量自动转换时,你需要当心潜在的数据丢失。例如,float转换成int时丢失信息(小数部分的信息)。如果你不是显示的将一个int赋值给float,大多数编译器会发出警告,或者甚至是错误。如果你确定左边的类型和右边的类型兼容,那么隐式的转换是没有问题的。

运算符

如果无法改变一个变量,那它还有什么用呢?下表给出了C++中最常用的运算符和它们的用法示例。注意C++中的运算符可以是双元的(操作两个表达式),单元的(操作单个表达式),或者三元的(操作三个表达式)。C++中只有一个三元操作符,在本章后面的“条件语句”节介绍。

操作符描述用法
=二元运算符,将右边的值赋给左边的表达式int i;
i = 3;
int j;
j = i;
!一元运算符,对一个表达式的true/false状态取反bool b = !true;
bool b2 = !b;
+二元运算符,加法运算int i = 3 + 2;
int j = i + 5;
int k = i + j;
-
*
/
减法、乘法,除法二元运算符int i = 5 - 1;
int j = 5 * 2;
int k = j / i;
%求除法余数的二元运算符。也叫作取模运算符int remainder = 5 % 2;
++将一个表达式增加1的一元运算符。如果运算符出现在表达式的右边或后自增,表达式的结果将不变。如果运算符出现在表达式的前面或前自增,表达式的结果是新值 i++;
++i;
--将表达式减1的一元运算符i--;
--i;
+= i = i + j的简写语法i += j;
-=
*=
/=
%=
下面的简写语法:
i = i - j;
i = i * j;
i = i / j;
i = i % j;
i -= j;
i *= j;
i /= j;
i %= j;
&
&=
对两个 表达式的原生位进行位“与”i = j & k;
j &= k;
|
|=
对两个表达式的原生位进行位“或”i = j | k;
j |= k;
<<
>>
<<=
>>=
对一个表达式的原生位的每一位向左或向右移动指定数值的位数i = i << 1;
i = i >> 4;
i <<= 1;
i >>= 4;
^
^=
对两个表达式进行按位异或操作i = i ^ j;
i ^= j;

下面的程序展示了最常用的变量类型和运算符。如果你不清楚变量和运算符是怎么工作的,那么就试着算出程序的输出然后再运行程序验证你的答案。

int someInteger = 256;
short someShort;
long someLong;
float someFloat;
double someDouble;
someInteger++;
someInteger *= 2;
someShort = static_cast<short>(someInteger);
someLong = someShort * 10000;
someFloat = someLong + 0.785f;
someDouble = static_cast<double>(someFloat) / 100000;
cout << someDouble << endl;

C++对表达式的计算顺序有一套准则。如果你有一行包含很多运算符的复杂代码,那么它的运算顺序可能就不明显了。因些将一个复杂的表达式分成几个小的表达式或者显示的用括号将它分组成子表达式就比较好。例如,除非你碰巧记住了C++运算符优先级表,否则下面这行代码就让人产生混淆。

int i = 34 + 8 * 2 + 21 / 7 % 2;

增加括号使用运算的顺序更加清晰:

int i = 34 + (8 * 2) + ( (21 / 7) % 2 );

对于这些自个在家练习的人来说,这两种方法是等价的,i的最终结果都是51。如果你认为C++计算表达式是从左向右的,那你计算出的值是1.C++优先计算/、*和%(从左向右的顺序),然后是加减,再然后是位运算。括号可以让你显示的告诉编译器某个运算需要单独计算。

类型

在C++中你可以使用基本的类型(int,bool等等)来构建自定义的复杂类型。一旦你成为一个有经验的C++程序员,你将很少使用下面这些从C中带过来的的技术,因为类更加的强大。但知道下面类型构建的方法依然很重要,这样才能认识语法。

枚举类型
一个整数的确代表序列中的一个值——数字的序列。枚举类型定义的自定义序列,可以让你声名的变量值来自这个序列。正如下面代码所示的这样,在国际象棋编程中,你可以用int表示每个棋子的角色。代表角色的整数是const类型的,表示它们不能被修改。

const int PieceTypeKing = 0;
const int PieceTypeQueen = 1;
const int PieceTypeRook = 2;
const int PieceTypePawn = 3;
//etc.
int myPiece = PieceTypeKing;

        虽然这看上去很好,但它很危险。因为棋子只是一个整数,如果另一位程序员增加一条语句将棋子的值增1会怎样?加1的话国王变成了王后,这很不正常。更糟糕的是,如果某人会将棋子赋值成-1,就没有对应的角色。
       枚举类型通过严格定义变量的数值范围来解决这些问题。下面的代码定义了一个新的PieceType类型,它有四个可取的值,代表四种象棋角色:

enum PieceType { PieceTypeKing, PieceTypeQueen, PieceTypeRook, PieceTypePawn };

       在表象之下,一个枚举类型仅仅是个整数值 。PieceTypeKing的真实值是0。然后,通过定义PieceType类型变量的可能取值,如果你试图对PieceType类型变量进行算术运算或者把它们当成整数编译器会发出警告或者报错。下面的代码声明了一个PieceType类型的变量,然后把它当成整数使用,在大多数编译器上将发出警告或者报错:

PieceType myPiece;
myPiece = 0;

         给一个枚举的成员指定整数值是可以的。语法如下:

enum PieceType { PieceTypeKing = 1, PieceTypeQueen, PieceTypeRook = 10, PieceTypePawn };

        这个例子中PieceTypeKing的值为1,编译器把PieceTypeQueen的值赋为2,PieceTypePawn的值为10,编译器自动将PieceTypePawn的值赋为11。如果没有给一个枚举成员指定值,那么编译器自动的把前一个枚举成员的值加1赋给它。如果第一个枚举成员没有指定值,编译器就把它设置成0。


        强枚举类型(Strongly Typed Enumerations)
        前面介绍的枚举类型不是强类型,这意味着它们不是类型安全的。它们通常被当作整数,所以两个不同枚举类型的值可以做比较。强类型enum class枚举修复了这个问题。例如下面的代码把前面定义的PieceType枚举改成了一个类型安全的版本:

enum class PieceType
{
    King = 1,
    Queen,
    Rook = 10,
    Pawn
};


          对于enum class,它不会把枚举值的名字自动暴露到闭包作用域,这意味着必须使用作用域解析符引用它们:

PieceType piece = PieceType::King;

          这也意味着可以给枚举值一个更短的名字,例如,King代表PieceTypeKing。另外,枚举值不会自动转换成整数,这意味下面的代码是非法的:

if (PieceType::Queen == 2) {...}

       枚举值的低层类型默认是整型,但是可以像下面这样更改它:

enum class PieceType : unsigned long
{
    King = 1,
    Queen,
    Rook = 10,
    Pawn
};

       注意 倡议使用强类型的enum class枚举来替代非类型安全的enum枚举。


结构体

        结构体可以把一个或多个类型打包成一个新类型。结构体经典的案例是一条数据库记录。如果你想构建一个人事系统来跟踪雇员信息,记录每位雇员的名字,姓氏,工号和薪水。下面展示了一个在employeestruct.h头文件中包含这些信息的结构体:

struct Employee {
    char firstInitial;
    char lastInitial;
    int employeeNumber;
    int salary;
};

          Employee类型声明的变量都会包含这些内置的成员。结构体的每一个成员都可以通过''."符号访问。下面的例子创建然后输出一个雇员的记录:

#include <iostream>
#include "employeestruct.h"
using namespace std;
int main()
{
    // Create and populate an employee.
    Employee anEmployee;
    anEmployee.firstInitial = 'M';
    anEmployee.lastInitial = 'G';
    anEmployee.employeeNumber = 42;
    anEmployee.salary = 80000;
    // Output the values of an employee.
    cout << "Employee: " << anEmployee.firstInitial <<
    anEmployee.lastInitial << endl;
    cout << "Number: " << anEmployee.employeeNumber << endl;
    cout << "Salary: $" << anEmployee.salary << endl;
    return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值