1. 引言
1.1 C++与C语言的关系
C++并非在空中凭空诞生,它深深地扎根于其前身:C语言。C++是由Bjarne Stroustrup在Bell实验室开发的,最初被命名为"C with Classes"(带有类的C语言),后来被重新命名为C++。"++"是C语言中的增量运算符,象征着C++是C语言的一个进步。C++在语法和设计上保留了很多C语言的元素,同时也引入了对象导向编程的概念,如类(class)和对象(object),并且在C++11以后的版本中,引入了一些现代编程语言的特性,比如智能指针(smart pointers),lambda表达式等。
在理解C++的基本数据类型时,我们通常会发现,C++与C语言在数据类型上有许多相似之处。然而,C++还引入了一些新的数据类型,以支持其更复杂的编程范式。因此,理解这些C++特有的数据类型,以及它们与C语言数据类型的区别,将使我们能更好地理解和使用C++。
1.2 对本文的简要概述
本文将深入探讨C++的基本数据类型,并在适当的地方对其与C语言进行对比。我们将从整型、浮点型、字符型、布尔型、枚举型和void类型开始,然后探讨复合类型,如数组、结构体、共用体、类、指针和引用。在理解了这些基本类型之后,我们将深入探讨C++11、C++14、C++17、C++20在数据类型上的改进和扩展。
内容丰富且涵盖广泛可能会让人觉得有些难以消化,但记住,金针在磨石上磨砺才能更加锋利。就像Friedrich Nietzsche的名言:“That which does not kill us makes us stronger”(那些未能杀死我们的东西,会让我们变得更强),通过深入理解和实践,我们可以更好地掌握C++的数据类型,进一步提升我们的编程能力。
在本文的最后,我们还将提供一些参考资料,以供读者进一步学习和参考。现在,让我们开始我们的旅程,深入探索C++的世界。
在前进的路上,每一步都是必要的,每一步都让我们更接近终点。就像著名的C++专家Scott Meyers在他的《Effective C++》一书中强调,要成为一个成功的C++程序员,不仅要理解C++的语法,更要理解C++的语义和哲学。让我们以开放的心态,接受新的知识,迈出掌握C++数据类型的第一步吧!
2. C++基本数据类型
2.1 整型
整型(integer)是我们在编程中最常见的数据类型之一。在C++中,整型可以有多种类型,其中包括 short
、int
、long
和 long long
。这些类型的大小可能根据不同的编译器和操作系统有所不同,但通常 short
是最小的,而 long long
是最大的。
接下来,让我们更详细地了解这些整型。
2.1.1 short, int, long, long long
这些类型的主要区别在于它们的大小和值的范围。下表比较了这些类型的典型大小和值范围:
类型 | 大小(字节) | 范围 |
short | 2 | -32,768 到 32,767 |
int | 4 | -2,147,483,648 到 2,147,483,647 |
long | 4 | -2,147,483,648 到 2,147,483,647 |
long long | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
这些值的范围是基于二进制补码表示法计算的。在实际编程中,我们可以通过 sizeof
运算符来确定特定系统中这些类型的具体大小。
#include <iostream> int main() { std::cout << "Size of short: " << sizeof(short) << " bytes" << std::endl; std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl; std::cout << "Size of long: " << sizeof(long) << " bytes" << std::endl; std::cout << "Size of long long: " << sizeof(long long) << " bytes" << std::endl; return 0; }
2.1.2 无符号与有符号整型
除了上述的基本整型,C++还提供了无符号(unsigned)版本的整型,包括 unsigned short
,unsigned int
,unsigned long
和 unsigned long long
。无符号整型只能表示非负整数,这意味着它们的范围从0开始。
unsigned int a = 10; // a is a non-negative integer
2.1.3 字面量
在C++中,我们可以通过字面量(literal)直接在代码中表示特定的值。对于整型,我们可以使用十进制、八进制、十六进制和二进制字面量。
int dec_val = 42; // decimal literal (base 10) int oct_val = 052; // octal literal (base 8) int hex_val = 0x2a; // hexadecimal literal (base 16) int bin_val = 0b101010; // binary literal (base 2, since C++14)
这里,052
是八进制字面量,0x2a
是十六进制字面量,0b101010
是二进制字面量。值得注意的是,二进制字面量在C++14及以后的版本中才被引入。
2.2 浮点型
浮点型(floating-point)是用来表示实数的,包括小数和大的科学记数。在C++中,浮点型有三种:float
,double
和 long double
。
2.2.1 float, double
float
和 double
是两种基本的浮点类型。double
的精度通常比 float
高,也就是说,double
能够表示更精确的数值。
float f = 3.14f; // f or F suffix for float double d = 3.14; // no suffix for double
long double
则提供了比 double
更高的精度,但其大小和精度可能因编译器和平台的不同而不同。
long double ld = 3.14l; // l or L suffix for long double
2.2.2 精度和范围
浮点数的精度和范围取决于其大小,但具体的值可能因编译器和平台的不同而不同。下表给出了这些类型的典型大小和精度:
类型 | 大小(字节) | 精度(有效数字位数) | 近似范围 |
float | 4 | 6-7 | 1.2E-38 to 3.4E+38 |
double | 8 | 15-16 | 2.3E-308 to 1.7E+308 |
long double | 12-16 | 19-20 | 3.4E-4932 to 1.1E+4932 (on most common platforms) |
2.2.3 字面量
浮点数的字面量可以由整数部分、小数部分和/或指数部分组成。指数是通过 ‘e’ 或 ‘E’ 表示的,表示10的幂。
double d1 = 3.14; // 3.14 double d2 = 6.02e23; // 6.02 x 10^23 double d3 = 1.6e-19; // 1.6 x 10^-19 double d4 = 0.1e1; // 0.1 x 10^1 = 1.0
2.3 字符型
字符型(char)用于表示单个字符。在C++中,它通常用来表示ASCII字符。但是,C++还提供了几种其他的字符类型:wchar_t
,char16_t
和char32_t
,它们用于表示宽字符或Unicode字符。
2.3.1 char, wchar_t, char16_t, char32_t
char
是最基本的字符类型,通常用于表示ASCII字符。
char c = 'a';
wchar_t
,char16_t
和 char32_t
用于表示宽字符或Unicode字符。
wchar_t wc = L'あ'; // wide character char16_t c16 = u'あ'; // Unicode character (since C++11) char32_t c32 = U'あ'; // Unicode character (since C++11)
注意,字符字面量前的 L
,u
和 U
前缀用于表示宽字符和Unicode字符。
2.3.2 字面量
字符的字面量是通过单引号表示的。
char c1 = 'a'; // character```cpp wchar_t c2 = L'あ'; // wide character char16_t c3 = u'あ'; // Unicode character (since C++11) char32_t c4 = U'あ'; // Unicode character (since C++11)
我们也可以用字符字面值来表示特殊的字符,比如换行符 (\n
)、制表符 (\t
) 等。
char newline = '\n'; char tab = '\t';
2.4 布尔型
布尔型(boolean)是一种特殊的数据类型,只有两个值:true
和 false
。在C++中,布尔类型被称为 bool
。
bool b1 = true; bool b2 = false;
布尔型常用于条件判断和循环控制。
if (b1) { // This block will execute because b1 is true } while (!b2) { // This loop will continue because b2 is false }
2.5 枚举型
枚举类型(enumeration)是一种用户定义的类型,它包括一组命名的值。在C++中,我们可以使用 enum
关键字创建枚举类型。
enum Color { RED, // 0 by default GREEN, // 1 BLUE // 2 };
我们可以创建枚举类型的变量,并使用枚举值来初始化它。
Color myColor = BLUE;
从C++11开始,我们还可以使用强类型枚举(也称为枚举类,enum class)。
enum class Fruit { APPLE, BANANA, CHERRY }; Fruit myFruit = Fruit::CHERRY;
枚举类提供了更好的类型安全,因为它们的枚举值不会隐式转换为整数,也不能与其他枚举类型的值进行比较。
2.6 Void类型
在C++中,void
是一种特殊的类型,它表示"无类型"。void
主要在三个地方使用:函数的返回类型、函数的参数以及指针的类型。
- 当函数不需要返回任何值时,我们可以使用
void
作为函数的返回类型。
void printHello() { std::cout << "Hello, world!" << std::endl; }
- 当函数不需要任何参数时,我们可以使用
void
作为函数的参数。
void doNothing(void) { // This function does nothing. }
- 我们可以声明一个类型为
void
的指针,它可以指向任何类型的对象。但是,我们不能直接通过void
指针访问对象,必须先将void
指针转换为具体类型的指针。
int i = 42; void *ptr = &i; int *int_ptr = static_cast<int*>(ptr); std::cout << *int_ptr << std::endl; // Outputs: 42
2.7 复合类型
除了基本数据类型,C++还提供了一些复合类型(compound types),包括数组、结构体、共用体、类、指针和引用。
2.7.1 数组
数组是由相同类型的元素组成的集合,其中所有元素在内存中连续存储。数组的大小在声明时必须确定,且在其生命周期内不能改变。
int nums[5] = {1, 2, 3, 4, 5};
2.7.2 结构体
结构体(struct)是一种用户定义的类型,可以容纳不同类型的数据。结构体在C++中主要用于组织和存储相关联的数据。
struct Student { std::string name; int age; }; Student s = {"Alice", 20};
2.7.3 共用体
共用体(union)与结构体类似,但在任何时候只能存储一个成员的值。换句话说,一个联合可以有多种类型,但只能代表一种类型。
union Mix { int i; double d; }; Mix m; m.d = 3.14; // Now the union holds a double value
2.7.4 类
类(class)是C++中最重要的特性之一,它代表了面向对象编程的核心。类是用户定义的类型,它定义了一组数据和对数据的操作。
class Circle { public: Circle(double r) : radius(r) {} double area() { return 3.14 * radius * radius; } private: double radius; }; Circle c(1.0); std::cout << c.area() << std::endl;
2.7.5 指针和引用
指针(pointer)和引用(reference)是C++中重要的复合类型。指针是存储另一种类型对象的内存地址的对象,而引用则是另一种类型对象的别名。
int i = 42; int *p = &i; // p is a pointer to i int &r = i; // r is a reference to i
以上就是C++的基本数据类型以及复合类型的介绍,了解了这些,我们就可以更好地理解C++程序的工作原理,也可以编写出更高效、更安全的## 2.7.6 函数指针
函数指针(Function Pointer)是指向函数的指针变量。和其他指针变量一样,函数指针指向的是存有特定值的内存地址。这个特定的值是什么呢?是函数在内存中的位置。函数指针可以用来调用函数,并提供函数作为其他函数的参数。函数指针对于实现高阶函数和回调函数(callback function)非常有用。
// 函数原型 void fun(int a, int b) { std::cout << "a + b = " << a + b << std::endl; } int main() { // 定义函数指针 p void (*p)(int, int); // 让 p 指向 fun 函数 p = fun; // 通过指针调用函数 (*p)(2, 3); // 输出:a + b = 5 return 0; }
在上述例子中,(*p)(2, 3);
和 fun(2, 3);
是等价的。这是因为 p 是指向 fun 函数的指针,通过 (*p)
我们得到的就是 fun 函数,所以 (*p)(2, 3);
就等价于 fun(2, 3);
。
2.7.7 函数引用
C++也支持函数引用,它是函数的别名,可以用来直接调用函数。和函数指针一样,函数引用也可以用来实现回调函数和高阶函数。
// 函数原型 void fun(int a, int b) { std::cout << "a + b = " << a + b << std::endl; } // 函数类型别名 using Fun = void(int, int); int main() { // 定义函数引用 Fun& f = fun; // 通过引用调用函数 f(2, 3); // 输出:a + b = 5 return 0; }
在上述例子中,f(2, 3);
和 fun(2, 3);
是等价的。这是因为 f 是 fun 函数的引用,所以 f(2, 3);
就等价于 fun(2, 3);
。
2.7.8 std::array和std::vector
除了上述的基本数据类型和复合类型,C++标准库还提供了一些其他的数据类型,如 std::array
和 std::vector
。
std::array
是一个固定大小的数组,它的大小在编译时需要确定。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::vector
是一种动态数组,它的大小可以在运行时改变。
std::vector<int> vec = {1, 2, 3, 4, 5}; vec.push_back(6); // Now the size of vec is 6
std::array
和 std::vector
提供了许多有用的方法,如 size()
、at()
、front()
、back()
等,使得我们可以更方便地操作数组。
以上就是C++的基本数据类型以及复合类型的介绍,了解了这些,我们就可以更好地理解C++程序的工作原理,也可以编写出更高效、更安全的代码。
3. C++与C语言数据类型的对比
虽然C++和C语言在很大程度上是相似的,但在数据类型方面,他们也存在着一些重要的差异。本节将探讨这些差异,以帮助您更好地理解这两种语言之间的联系与区别。
3.1 数据类型的差异
- 布尔类型:C++有一个内建的布尔类型
bool
,其值可以是true
或false
。虽然C语言中没有内建的布尔类型,但它通常使用int
来表示布尔值,0
表示false
,非零值表示true
。 - 字符串类型:在C++中,字符串可以被表示为
std::string
对象。std::string
类提供了许多方便的方法,如获取字符串的长度、连接字符串等。而在C语言中,字符串通常表示为字符数组。 - 复合类型:C++提供了类(class)和引用(reference),这是C语言中没有的。类是面向对象编程的基础,而引用提供了对另一个变量的别名,可以作为函数参数,提高代码的效率和易读性。
- 新的数据类型:C++11及其后续版本引入了一些新的数据类型,如
auto
用于自动类型推断,nullptr
用于空指针,enum class
用于强类型枚举等。
3.2 命名约定的差异
在C语言中,所有的类型名(包括结构体和联合体)都位于同一命名空间中。而在C++中,结构体和联合体有自己的命名空间,这意味着我们可以在不同的结构体中使用相同的成员名,而不会发生冲突。
3.3 使用场景的差异
由于C++支持面向对象编程,所以在设计数据结构和算法时,我们往往会使用类和对象。而在C语言中,我们通常会使用结构体和函数。
此外,C++中的数据类型通常更加灵活和强大。例如,std::vector
可以动态地调整大小,std::string
可以方便地处理字符串,std::map
和std::set
可以方便地处理键值对和集合等。
3.4 初始化的差异
- 统一初始化:C++11引入了统一的初始化语法,使用花括号
{}
进行初始化。这种初始化方式可以用于任何数据类型,包括数组、结构体、类和其他复合类型。例如,我们可以使用int a{5};
来初始化一个整数,或者使用std::vector v{1, 2, 3};
来初始化一个向量。这种初始化方式提供了更强的类型检查,可以避免某些类型的隐式转换。 - 列表初始化:在C++中,我们可以使用列表初始化来初始化数组或容器。例如,
int arr[] = {1, 2, 3};
或std::vector vec = {4, 5, 6};
。而在C语言中,只能使用列表初始化来初始化数组。 - 构造函数初始化:C++中的类可以有构造函数,这允许我们在创建对象时进行初始化。例如,
std::string str("Hello");
。C语言没有类和构造函数的概念,所以不能使用这种方式进行初始化。 - 默认初始化:在C++中,局部变量默认不进行初始化,这可能导致未定义的行为。但是,类的成员变量和全局变量会被自动初始化为默认值。而在C语言中,只有全局变量和静态变量会被自动初始化为默认值。
3.5 类型推断的差异
auto
关键字:C++11引入了auto
关键字,允许编译器自动推断变量的类型。这在处理复杂的数据类型,如迭代器或lambda表达式时非常有用。例如,auto it = vec.begin();
。C语言中没有这种类型推断的功能。
3.6 类型别名的差异
typedef
与using
:在C语言中,我们使用typedef
来为类型创建别名。而在C++中,除了typedef
之外,还可以使用using
关键字来创建类型别名,这在模板编程中尤为有用。例如,using VecInt = std::vector;
。
3.7 整数类型初始化的差异
- 函数风格的类型转换:在C++中,我们可以使用函数风格的类型转换来初始化或转换数据类型。例如,
int a = int(3.14);
会将浮点数3.14
转换为整数3
。这种转换方式在C++中是有效的,但在C语言中是不支持的。 - 静态类型转换:除了函数风格的类型转换,C++还提供了
static_cast
来进行类型转换。例如,int b = static_cast(3.14);
。这种转换方式提供了更明确的类型转换意图,并且在某些情况下比C语言的强制类型转换更安全。
这两种初始化和转换方式在C++中都是常见的,但在C语言中是不可用的。这也是C++和C语言在数据类型处理上的另一个重要差异。
4. C++11、C++14、C++17、C++20在数据类型上的改进和扩展
从C++11开始,C++在每个新的版本中都引入了许多新的数据类型和特性。这些新的特性在很大程度上提高了C++的表达能力和效率,使得编程更加方便和强大。
4.1 自动类型推断(auto)
C++11引入了auto
关键字,用于自动类型推断。这意味着,编译器可以根据初始化表达式的类型自动推断变量的类型。
auto i = 42; // i has type int auto d = 3.14; // d has type double auto s = "hello"; // s has type const char* auto v = std::vector<int>{1, 2, 3}; // v has type std::vector<int>
auto
关键字可以大大减少代码的冗余,并提高编程效率。例如,当我们需要声明一个复杂类型的变量时,auto
可以帮助我们简化代码。
std::map<std::string, std::vector<int>>::iterator it = m.begin(); // without auto auto it = m.begin(); // with auto
4.2 基于范围的for循环
C++11引入了基于范围的for循环(range-based for loop),这使得对容器(如数组和std::vector
)的遍历变得更加简单和清晰。
std::vector<int> v = {1, 2, 3, 4, 5}; for (int i : v) { std::cout << i << std::endl; }
在这个例子中,i : v
表示"在v
中的每个元素i
"。这种语法使得代码更加易读和直观。
4.3 nullptr和强类型枚举
在C++11中,引入了nullptr
关键字,用于表示空指针。在之前的版本中,我们通常使用NULL
或0
来表示空指针,但这会导致一些问题,因为NULL
和0
也可以表示整数。nullptr
是一种特殊类型的字面量,它只能转换为指针类型,这提高了代码的安全性和清晰性。
int* p1 = nullptr; // OK int i = nullptr; // Error: nullptr cannot convert to int
C++11还引入了强类型枚举(enum class)。在之前的版本中,枚举类型的值可以隐式转换为整数,这会导致一些问题。强类型枚举的值不能隐式转换为其他类型,这提高了代码的安全性。
enum class Color {RED, GREEN, BLUE}; Color c = Color::RED; int i = c; // Error: Color::RED cannot convert to int
4.4 可选类型(optional)
C++17引入了std::optional
,这是一种可以存储值或不存储值的容器。当我们的函数可能不返回值时(例如,查找操作可能找不到元素),我们可以使用std::optional
来表示这种情况。
std::optional<int> find(const std::vector<int>& v, int x) { for (int i : v) { if (i == x) return i; } return std::nullopt; // or return {}; } auto result = find(v, 42); if (result.has_value()) { std::cout << "Found: " <<*result << std::endl; } else { std::cout << "Not found" << std::endl; }
在这个例子中,如果找到了元素,find
函数会返回该元素;否则,它会返回一个不含有值的std::optional
。我们可以使用has_value
成员函数来检查std::optional
是否含有值。
4.5 任意类型(any)
C++17还引入了std::any
类型,它可以存储任意类型的值,类似于C#的object
类型或Java的Object
类型。我们可以使用std::any_cast
来获取存储在std::any
中的值。
std::any a = 42; try { std::cout << std::any_cast<int>(a) << std::endl; } catch (const std::bad_any_cast& e) { std::cout << "Bad any cast: " << e.what() << std::endl; }
在这个例子中,如果std::any_cast
不能将std::any
转换为指定的类型,它会抛出一个std::bad_any_cast
异常。
4.6 变体类型(variant)
C++17还引入了std::variant
类型,它可以存储多种但数量有限的类型的值。std::variant
是一个类型安全的联合体,我们可以使用std::get
或std::get_if
来获取存储在std::variant
中的值。
std::variant<int, double, std::string> v; v = 3.14; try { std::cout << std::get<double>(v) << std::endl; } catch (const std::bad_variant_access& e) { std::cout << "Bad variant access: " << e.what() << std::endl; }
在这个例子中,如果std::get
不能将std::variant
转换为指定的类型,它会抛出一个std::bad_variant_access
异常。
以上就是C++11、C++14、C++17和C++20在数据类型上的一些主要改进和扩展。这些新的特性在很大程度上提高了C++的表达能力和效率,使得编程更加方便和强大。
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。