数据类型
七种基本的 C++ 数据类型
类型 | 关键字 |
---|---|
布尔型 | bool |
字符型 | char |
整型 | int |
浮点型 | float |
双浮点型 | double |
无类型 | void |
宽字符型 | wchar_t |
一些基本类型可以使用一个或多个类型修饰符进行修饰:
- signed:表示变量可以存储负数。对于整型变量来说,signed 可以省略,因为整型变量默认为有符号类型
- unsigned:表示变量不能存储负数。对于整型变量来说,unsigned 可以将变量范围扩大一倍
- short:表示变量的范围比 int 更小。short int 可以缩写为 short
- long:表示变量的范围比 int 更大。long int 可以缩写为 long
- long long:表示变量的范围比 long 更大。C++11 中新增的数据类型修饰符
各数据类型的内存和范围
下表显示了各种数据类型在内存中存储值时需要占用的内存,以及该类型的变量所能存储的最大值和最小值
类型 | 位 | 范围 |
---|---|---|
char | 1 个字节 | -128 到 127 或者 0 到 255 |
unsigned char | 1 个字节 | 0 到 255 |
signed char | 1 个字节 | -128 到 127 |
int | 4 个字节 | -2147483648 到 2147483647 |
unsigned int | 4 个字节 | 0 到 4294967295 |
signed int | 4 个字节 | -2147483648 到 2147483647 |
short int | 2 个字节 | -32768 到 32767 |
unsigned short int | 2 个字节 | 0 到 65,535 |
signed short int | 2 个字节 | -32768 到 32767 |
long int | 8 个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
signed long int | 8 个字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
unsigned long int | 8 个字节 | 0 到 18,446,744,073,709,551,615 |
float | 4 个字节 | 精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字) |
double | 8 个字节 | 双精度型占8 个字节(64位)内存空间,+/- 1.7e +/- 308 (~15 个数字) |
long double | 16 个字节 | 长双精度型 16 个字节(128位)内存空间,可提供18-19位有效数字 |
wchar_t | 2 或 4 个字节 | 1 个宽字符 |
枚举类型
例如,下面的代码定义了一个颜色枚举,变量 c 的类型为 color。最后,c 被赋值为 "blue"
cpp
enum color { red, green, blue } c;
c = blue;
类型判断
-
typeid
运算符:可以用于获取一个对象的类型信息,返回一个type_info
对象(类型的首字母)。例如:cppint i = 42; float j = 42.5f; std::cout << typeid(i).name() << std::endl; // 输出:i std::cout << typeid(j).name() << std::endl; // 输出:f
-
std::is_same
类型特征:可以用于检查两种类型是否相同。例如:cppstd::cout << std::is_same<int, float>::value << std::endl; // 输出:0 std::cout << std::is_same<int, int>::value << std::endl; // 输出:1
-
std::is_integral
类型特征:可以用于检查一个类型是否为整型。例如:cppstd::cout << std::is_integral<int>::value << std::endl; // 输出:1 std::cout << std::is_integral<float>::value << std::endl; // 输出:0
-
std::is_floating_point
类型特征:可以用于检查一个类型是否为浮点型。例如:cppstd::cout << std::is_floating_point<int>::value << std::endl; // 输出:0 std::cout << std::is_floating_point<float>::value << std::endl; // 输出:1
-
std::is_pointer
类型特征:可以用于检查一个类型是否为指针类型。例如:cppstd::cout << std::is_pointer<int*>::value << std::endl; // 输出:1 std::cout << std::is_pointer<float>::value << std::endl; // 输出:0
-
std::is_array
类型特征:可以用于检查一个类型是否为数组类型。例如:cppstd::cout << std::is_array<int[]>::value << std::endl; // 输出:1 std::cout << std::is_array<float>::value << std::endl; // 输出:0
-
std::is_function
类型特征:可以用于检查一个类型是否为函数类型。例如:cppstd::cout << std::is_function<int(int)>::value << std::endl; // 输出:1 std::cout << std::is_function<float>::value << std::endl; // 输出:0
这些类型判断方式可以帮助我们在编写泛型代码时更加灵活和安全
类型转换
隐式类型转换
简单粗暴,但是存在问题。例如将一个 float
类型的值 42.5
转换为 int
类型,由于 int
类型不支持小数部分,因此在进行转换时,小数部分会被截断,只保留整数部分
cpp
float f = 42.5f;
int i = int(f);
int i = (int)f;
int i = f;
显式类型转换
C 风格的类型转换:使用括号将需要转换的类型括起来,并在前面添加需要转换的类型。例如:
cpp
int i = 42;
float f = (float)i; // C 风格的类型转换
C++ 风格的类型转换:使用 static_cast
、dynamic_cast
、const_cast
或 reinterpret_cast
进行类型转换。例如:
静态转换static_cast
是将一种数据类型的值强制转换为另一种数据类型的值
cpp
int i = 42;
float f = static_cast<float>(i);
动态转换dynamic_cast
通常用于将一个基类 指针或引用转换为派生类指针或引用
cpp
class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针
常量转换const_cast
用于将 const 类型 的对象转换为非 const 类型的对象
cpp
const int i = 10;
int& r = const_cast<int&>(i); // 常量转换,将const int转换为int
重新解释转换reinterpret_cast
将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换
cpp
int i = 10;
float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型
数字和字符串互转
- 字符串转 int、float
cpp
#include <iostream>
#include <string>
int main() {
std::string str = "42";
int i = std::stoi(str);
std::cout << "The integer is: " << i << std::endl;
float f = std::stof(str);
std::cout << "The float is: " << f << std::endl;
}
这些函数都需要包含头文件 <string>
,接受一个字符串作为参数。如果字符串不包含有效的数字,则会抛出 std::invalid_argument
或 std::out_of_range
异常。
需要注意的是,这些函数只能将符合特定格式的字符串转换为数字类型。例如,对于 std::stoi
,字符串必须以数字开头,可以包含正负号,但不能包含其他字符。对于 std::stof
,字符串必须包含小数点和数字,可以包含正负号和指数符号,但不能包含其他字符
- int、float 转字符串
cpp
#include <iostream>
#include <string>
int main() {
int i = 42;
float f = 3.14;
std::string str1 = std::to_string(i);
std::string str2 = std::to_string(f);
std::cout << "The integer string is: " << str1 << std::endl;
std::cout << "The float string is: " << str2 << std::endl;
}
常量
在 C++ 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器。
- 使用 const 关键字。
define 预处理器
下面是使用 #define 预处理器定义常量的形式:
cpp
#define identifier value
具体请看下面的实例:
cpp
#include <iostream>
using namespace std;
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
int main()
{
int area;
area = LENGTH * WIDTH;
cout << area;
cout << NEWLINE;
}
const 关键字
您可以使用 const 前缀声明指定类型的常量,如下所示:
cpp
const type variable = value;
具体请看下面的实例:
cpp
#include <iostream>
using namespace std;
int main()
{
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
cout << area;
cout << NEWLINE;
}
类型限定符
类型限定符提供了变量的额外信息,用于在定义变量或函数时改变它们的默认行为的关键字。
限定符 | 含义 |
---|---|
const | const 定义常量,表示该变量的值不能被修改。。 |
volatile | 修饰符 volatile 告诉该变量的值可能会被程序以外的因素改变,如硬件或其他线程 |
restrict | 由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict |
mutable | 表示类中的成员变量可以在 const 成员函数中被修改 |
static | 用于定义静态变量,表示该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问 |
register | 用于定义寄存器变量,表示该变量被频繁使用,可以存储在CPU的寄存器中,以提高程序的运行效率。 |
const 实例
cpp
const int NUM = 10; // 定义常量 NUM,其值不可修改
const int* ptr = &NUM; // 定义指向常量的指针,指针所指的值不可修改
int const* ptr2 = &NUM; // 和上面一行等价
volatile 实例
cpp
volatile int num = 20; // 定义变量 num,其值可能会在未知的时间被改变
mutable 实例
cpp
class Example {
public:
int get_value() const {
return value_; // const 关键字表示该成员函数不会修改对象中的数据成员
}
void set_value(int value) const {
value_ = value; // mutable 关键字允许在 const 成员函数中修改成员变量
}
private:
mutable int value_;
};
static 实例
cpp
void example_function() {
static int count = 0; // static 关键字使变量 count 存储在程序生命周期内都存在
count++;
}
register 实例
cpp
void example_function(register int num) {
// register 关键字建议编译器将变量 num 存储在寄存器中
// 以提高程序执行速度
// 但是实际上是否会存储在寄存器中由编译器决定
}
变量作用域
有三个地方可以定义变量:
- 在函数或一个代码块内部声明的变量,称为局部变量
- 在函数参数的定义中声明的变量,称为形式参数
- 在所有函数外部声明的变量,称为全局变量
局部变量和全局变量
例子:
cpp
#include <iostream>
using namespace std;
int g; // 全局变量声明
int main ()
{
int a, b; // 局部变量声明
// 实际初始化
a = 10;
b = 20;
g = a + b;
cout << g;
}
局部变量的值会覆盖全局变量的值,例子:
cpp
#include <iostream>
using namespace std;
int g; // 全局变量声明
int main ()
{
int g = 10; // 局部变量声明
cout << g;
}
变量的作用域可以分为以下几种:
- 局部作用域:在函数内部声明的变量具有局部作用域,它们只能在函数内部访问。局部变量在函数每次被调用时被创建,在函数执行完后被销毁
- 全局作用域:在所有函数和代码块之外声明的变量具有全局作用域,它们可以被程序中的任何函数访问。全局变量在程序开始时被创建,在程序结束时被销毁
- 块作用域:在代码块内部声明的变量具有块作用域,它们只能在代码块内部访问。块作用域变量在代码块每次被执行时被创建,在代码块执行完后被销毁
- 类作用域:在类内部声明的变量具有类作用域,它们可以被类的所有成员函数访问。类作用域变量的生命周期与类的生命周期相同
块作用域
cpp
#include <iostream>
int main() {
int a = 10;
{
int a = 20; // 块作用域变量
std::cout << "块变量: " << a << std::endl;
}
std::cout << "外部变量: " << a << std::endl;
}
// 输出结果
块变量: 20
外部变量: 10
类作用域
可以使用类名和作用域解析运算符 ::
来访问这个变量
cpp
#include <iostream>
class MyClass {
public:
static int class_var; // 类作用域变量
};
int MyClass::class_var = 30; // 使用类名和作用域解析运算符 :: 来访问这个变量
int main() {
std::cout << "类变量: " << MyClass::class_var << std::endl;
}
C++ 内存分区
- 全局区 (Global)
全局区是存放全局变量 和静态变量 的内存区域,在程序启动 时自动分配,在程序结束 时自动释放。全局区的内存空间是连续的,由编译器自动管理。全局区的大小也是固定 的,因此只能存放较小的数据
- 常量区 (Const)
常量区是存放常量数据 的内存区域,如字符串常量、全局常量等。常量区内存只读,不可修改 。常量区的内存空间是连续的,由编译器自动管理
- 栈区 (Stack)
栈区是由编译器自动分配和释放的内存区域,存放函数的参数值、局部变量 等。栈区内存的分配和释放速度很快 ,因为它的内存空间是连续 的,且由编译器自动管理。栈区的大小是固定的,一般只能存放较小 的数据。当函数执行完毕后,栈区内存会自动释放,因此不需要手动释放栈区内存
- 堆区 (Heap)
堆区是由程序员手动分配和释放的内存区域,存放程序运行期间动态分配的内存。堆区的内存空间是不连续 的,因此内存分配和释放的速度较慢 ,但是堆区的内存空间较大 ,可以存放较大的数据。堆区内存的分配和释放需要使用 new
和 delete
或 malloc
和 free
等函数手动管理
- 代码区 (Code)
代码区是存放程序的可执行代码的内存区域,由操作系统负责管理。代码区的内存空间是只读的,不可修改
运算符
算术运算符
下表显示了 C++ 支持的算术运算符。
假设变量 A 的值为 10,变量 B 的值为 21,则:
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 31 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -11 |
* | 把两个操作数相乘 | A * B 将得到 210 |
/ | 取整 | B / A 将得到 2 |
% | 取余 | B % A 将得到 1 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
-- | 自减运算符,整数值减少 1 | A-- 将得到 9 |
关系运算符
下表显示了 C++ 支持的关系运算符。
假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 不为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 不为真。 | |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 不为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
下表显示了 C++ 支持的关系逻辑运算符。
假设变量 A 的值为 1,变量 B 的值为 0,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 | (A && B) 为 false。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 | (A || B) 为 true。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 | !(A && B) 为 true。 |
其他一些重要的运算符
运算符 | 描述 |
---|---|
sizeof | sizeof 运算符返回变量的大小。例如,sizeof(a) 将返回 4,其中 a 是整数。 |
Condition ? X : Y | 条件运算符。如果 Condition 为真 ? 则值为 X : 否则值为 Y。 |
,(逗号运算符) | 逗号运算符会顺序执行一系列运算。整个逗号表达式的值是以逗号分隔的列表中的最后一个表达式的值。 |
.(点运算符)和 ->(箭头运算符) | 成员运算符用于引用类、结构和共用体的成员 |
::(双冒号运算符) | 用于直接访问命名空间、类、结构体、共用体或枚举类型的成员或静态成员 |
Cast(强制转换运算符) | 强制类型转换,把一种数据类型转换为另一种数据类型。例如,int(2.2000) 将返回 2 |
&(指针运算符) | 指针运算符 & 由变量获取到它的地址 。例如 &a 得到变量 a 的地址 |
*(指针运算符) | 指针运算符 * 由地址获取到变量的值 。例如 *ptr 得到地址 ptr 指向的变量 |
逗号运算符(,)
- 使用逗号运算符是为了把几个表达式放在一起
- 整个逗号表达式的值为系列中最后一个表达式的值
例子:
cpp
#include <iostream>
using namespace std;
int main()
{
int i, j;
j = 10;
i = (j++, j+100, 999+j);
cout << i; // 1010
}
成员运算符(. ->)
成员运算符用于引用类、结构和共用体的(public)成员
cpp
class Point {
public:
int x;
int y;
void print() {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
/** 点运算符 */
Point p;
p.x = 5; // 访问成员变量
p.print(); // 访问成员函数
/* 箭头运算符 */
Point* pPtr = new Point();
pPtr->x = 5; // 访问成员变量
pPtr->print(); // 访问成员函数
delete pPtr;
}
双冒号运算符(::)
双冒号运算符用于访问命名空间、枚举类型、类的成员
以下例子中演示访问命名空间 NS 中的成员变量 x,其实 std 也是一个命名空间,其包含了 C++ 标准库中的所有标识符,例如标准输入输出流、容器、算法等等
cpp
/* 命名空间 */
namespace NS {
int x = 42;
}
/* 类 */
class MyClass {
public:
static int y;
};
/* 枚举类型 */
enum Color { RED, GREEN, BLUE };
int MyClass::y = 123; // 访问类的静态成员
int main() {
std::cout << NS::x << std::endl; // 访问命名空间成员
std::cout << MyClass::y << std::endl; // 访问类的静态成员
std::cout << Color::RED << std::endl; // 访问枚举类型成员
}
基类也是类的一种,所以访问基类的成员也用双冒号运算符
cpp
class Base {
public:
int x;
void print() {
std::cout << "Base::print()" << x << std::endl;
}
};
class Derived : public Base {
public:
void print() {
Base::print(); // 调用基类的方法
std::cout << "Derived::print()" << Base::x << std::endl; // 访问基类的成员
}
};
int main() {
Derived d;
d.x = 42;
d.print();
}
指针运算符(& *)
cpp
#include <iostream>
using namespace std;
int main ()
{
int var;
int *ptr; // * 运算符也可以用来表示一个指针
int val;
var = 3000;
ptr = &var; // 获取变量 var 的地址,赋值给 ptr
val = *ptr; // 获取地址 ptr 指向的变量 var 的值
cout << "Value of var :" << var << endl;
cout << "Value of ptr :" << ptr << endl;
cout << "Value of val :" << val << endl;
return 0;
}
注释
C++ 存在三种注释:
//
一般用于单行注释/* ... */
一般用于多行注释#if 0 ... #endif
条件编译注释
cpp
#include <iostream>
using namespace std;
int main() {
// 这是单行注释
/* 这是注释 */
/*
* 可以多行注释
*/
cout << "Hello World!";
return 0;
}
块注释用于程序调试,测试时使用 #if 1 来执行测试代码,发布后使用 #if 0 来屏蔽测试代码
cpp
#if condition
code1
#else
code2
#endif
基本的输入和输出
I/O 库头文件
下列的头文件在 C++ 编程中很重要。
头文件 | 函数和描述 |
---|---|
<iostream> |
该文件定义了 cin、cout、cerr 和 clog 对象,分别对应于标准输入流、标准输出流、非缓冲标准错误流和缓冲标准错误流。 |
<iomanip> |
该文件通过所谓的参数化的流操纵器(比如 setw 和 setprecision),来声明对执行标准化 I/O 有用的服务。 |
<fstream> |
该文件为用户控制的文件处理声明服务。我们将在文件和流的相关章节讨论它的细节。 |
标准输出流(cout)
cpp
#include <iostream>
using namespace std;
int main( )
{
char str[] = "Hello C++";
cout << "Value of str is : " << str << endl;
}
C++ 编译器根据要输出变量的数据类型,选择合适的流插入运算符来显示值。<< 运算符被重载来输出内置类型(整型、浮点型、double 型、字符串和指针)的数据项。
流插入运算符 << 在一个语句中可以多次使用 ,如上面实例中所示,endl 用于在行末添加一个换行符
标准输入流(cin)
cpp
#include <iostream>
using namespace std;
int main( )
{
char name[50];
cout << "请输入您的名称: ";
cin >> name;
cout << "您的名称是: " << name << endl;
}
流提取运算符 >> 在一个语句中可以多次使用,如果要求输入多个数据,可以使用如下语句:
cpp
cin >> name >> age;
这相当于下面两个语句:
cpp
cin >> name;
cin >> age;
标准错误流(cerr)
预定义的对象 cerr
是 iostream
类的一个实例。cerr 对象附属到标准输出设备,通常也是显示屏,但是 cerr
对象是非缓冲 的,且每个流插入到 cerr 都会立即输出
cpp
#include <iostream>
using namespace std;
int main( )
{
char str[] = "Unable to read....";
cerr << "Error message : " << str << endl;
}
标准日志流(clog)
预定义的对象 clog
是 iostream
类的一个实例。clog 对象附属到标准输出设备,通常也是显示屏,但是 clog
对象是缓冲 的。这意味着每个流插入到 clog 都会先存储在缓冲 区,直到缓冲填满 或者缓冲区刷新时才会输出
cpp
#include <iostream>
using namespace std;
int main( )
{
char str[] = "Unable to read....";
clog << "Error message : " << str << endl;
}
日期和时间
C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用 <ctime>
头文件。
有四个与时间相关的类型:clock_t、time_t、size_t 和 tm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数。
结构类型 tm 把日期和时间以 C 结构的形式保存,tm 结构的定义如下:
cpp
struct tm {
int tm_sec; // 秒,正常范围从 0 到 59,但允许至 61
int tm_min; // 分,范围从 0 到 59
int tm_hour; // 小时,范围从 0 到 23
int tm_mday; // 一月中的第几天,范围从 1 到 31
int tm_mon; // 月,范围从 0 到 11
int tm_year; // 自 1900 年起的年数
int tm_wday; // 一周中的第几天,范围从 0 到 6,从星期日算起
int tm_yday; // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起
int tm_isdst; // 夏令时
};
下面是 C/C++ 中关于日期和时间的重要函数。所有这些函数都是 C/C++ 标准库的组成部分,您可以在 C++ 标准库中查看一下各个函数的细节。
函数 | 描述 |
---|---|
time_t time(time_t *time); | 该函数返回系统的当前日历时间,自 1970 年 1 月 1 日以来经过的秒数。如果系统没有时间,则返回 -1 |
char *ctime(const time_t *time); | 该返回一个表示当地时间的字符串指针,字符串形式 day month year hours:minutes:seconds year\n\0 |
struct tm *localtime(const time_t *time); | 该函数返回一个指向表示本地时间的 tm 结构的指针 |
clock_t clock(void); | 该函数返回程序执行起(一般为程序的开头),处理器时钟所使用的时间。如果时间不可用,则返回 -1 |
char * asctime ( const struct tm * time ); | 该函数返回一个指向字符串的指针,字符串包含了 time 所指向结构中存储的信息,返回形式为:day month date hours:minutes:seconds year\n\0。 |
struct tm *gmtime(const time_t *time); | 该函数返回一个指向 time 的指针,time 为 tm 结构,用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示 |
time_t mktime(struct tm *time); | 该函数返回日历时间,相当于 time 所指向结构中存储的时间 |
double difftime ( time_t time2, time_t time1 ); | 该函数返回 time1 和 time2 之间相差的秒数 |
size_t strftime(); | 该函数可用于格式化日期和时间为指定的格式 |
当前日期和时间
下面的实例获取当前系统的日期和时间,包括本地时间和协调世界时(UTC)
cpp
#include <iostream>
#include <ctime>
using namespace std;
int main()
{
// 基于当前系统的当前日期/时间
time_t now = time(0);
// 把 now 转换为字符串形式
char* dt = ctime(&now);
cout << "本地日期和时间:" << dt << endl;
// 把 now 转换为 tm 结构
tm *gmtm = gmtime(&now);
dt = asctime(gmtm);
cout << "UTC 日期和时间:"<< dt << endl;
}
输出结果:
本地日期和时间:Fri Sep 15 06:44:51 2023
UTC 日期和时间:Fri Sep 15 06:44:51 2023
使用结构 tm 格式化时间
C 库函数 size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr
根据 format
中定义的格式化规则,格式化结构 timeptr
表示的时间,并把它存储在 str
中。
下面是 strftime() 函数的声明
cpp
size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)
- str -- 这是指向目标数组的指针,用来复制产生的 C 字符串。
- maxsize -- 这是被复制到 str 的最大字符数。
- format -- 这是 C 字符串,包含了普通字符和特殊格式说明符的任何组合。这些格式说明符由函数替换为表示 tm 中所指定时间的相对应值。格式说明符是:
cpp
#include <stdio.h>
#include <time.h>
int main ()
{
time_t rawtime;
struct tm *info;
char buffer[80];
time( &rawtime );
info = localtime( &rawtime );
strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", info);
printf("格式化的日期 & 时间 : %s\n", buffer );
}
输出结果:
格式化的日期 & 时间 : 2023-09-15 06:45:54
判断
判断语句
C++ 编程语言提供了以下类型的判断语句。点击链接查看每个语句的细节。
语句 | 描述 |
---|---|
if 语句 | 一个 if 语句 由一个布尔表达式后跟一个或多个语句组成 |
if...else 语句 | 一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行 |
嵌套 if 语句 | 您可以在一个 if 或 else if 语句内使用另一个 if 或 else if 语句 |
switch 语句 | 一个 switch 语句允许测试一个变量等于多个值时的情况 |
嵌套 switch 语句 | 您可以在一个 switch 语句内使用另一个 switch 语句 |
? : 运算符
我们已经在前面的章节中讲解了 条件运算符 ? :,可以用来替代 if...else 语句。它的一般形式如下:
cpp
Exp1 ? Exp2 : Exp3;
其中,Exp1、Exp2 和 Exp3 是表达式。请注意,冒号的使用和位置
? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个 ? 表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个 ? 表达式的值
循环
循环类型
C++ 编程语言提供了以下几种循环类型。点击链接查看每个类型的细节。
循环类型 | 描述 |
---|---|
while 循环 | 当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件 |
for 循环 | 多次执行一个语句序列,简化管理循环变量的代码 |
do...while 循环 | 除了它是在循环主体结尾测试条件外,其他与 while 语句类似 |
嵌套循环 | 您可以在 while、for 或 do..while 循环内使用一个或多个循环 |
循环控制语句
循环控制语句更改执行的正常序列。当执行离开一个范围时,所有在该范围中创建的自动对象都会被销毁。
C++ 提供了下列的控制语句。点击链接查看每个语句的细节。
控制语句 | 描述 |
---|---|
break 语句 | 终止 loop 或 switch 语句,程序流将继续执行紧接着 loop 或 switch 的下一条语句 |
continue 语句 | 引起循环跳过主体的剩余部分,立即重新开始测试条件 |
goto 语句 | 将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句 |
字符串
C++ 标准库提供了 string 类类型,需要引入 #include <string>
1. 构造字符串
cpp
string s1(); // si = ""
string s2("Hello"); // s2 = "Hello"
string s3(4, 'K'); // s3 = "KKKK"
string s4("12345", 1, 3); //s4 = "234",即 "12345" 的从下标 1 开始,长度为 3 的子串
注意:string 类不接收一个整型参数或一个字符型参数的构造函数。下面的两种写法是错误的
cpp
string s1('K'); // 不接收一个字符型参数
string s2(123); // 不接收一个整型参数
2. 求字符串长度
cpp
string s1 = "hello world";
cout << s1.length() << endl; // 11
cout << s1.size() << endl; // 11
3. 字符串拼接
除了可以使用+
和+=
运算符对 string 对象执行字符串的连接操作外,string 类还有 append
成员函数,可以用来向字符串后面添加内容。append 成员函数返回对象自身的引用,会改变原字符串。例如:
cpp
string s1("123"), s2("abc");
s1.append(s2); // s1 = "123abc"
s1.append(s2, 1, 2); // s1 = "123abcbc"
s1.append(3, 'K'); // s1 = "123abcbcKKK"
s1.append("ABCDE", 2, 3); // s1 = "123abcbcKKKCDE",添加 "ABCDE" 的子串(2, 3)
补充见 c.biancheng.net/view/400.ht...
指针
什么是内存地址
每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示这个变量在内存中的地址
cpp
#include <iostream>
using namespace std;
int main ()
{
int var1;
char var2[10];
cout << "var1 变量的地址: ";
cout << &var1 << endl;
cout << "var2 变量的地址: ";
cout << &var2 << endl;
return 0;
}
运行结果:
var1 变量的地址: 0xbfebd5c0
var2 变量的地址: 0xbfebd5b6
什么是指针
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
cpp
type *var-name;
在这里,type 是指针的基类型,它必须是一个有效的 C++ 数据类型,var-name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
cpp
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同
指针的使用
指针的使用一般包括:
- 定义一个指针变量
- 把变量地址赋值给指针
- 访问指针变量中可用地址的值
cpp
#include <iostream>
using namespace std;
int main ()
{
int var = 20; // 实际变量的声明
int *ip; // 指针变量的声明
ip = &var; // 在指针变量中存储 var 的地址
cout << "变量 var 的值是:";
cout << var << endl;
// 输出在指针变量中存储的地址
cout << "指针 ip 指向的地址是:";
cout << ip << endl;
// 访问指针中地址的值
cout << "指针 ip 指向的地址存的变量的值是:";
cout << *ip << endl;
return 0;
}
运行结果:
csharp
变量 var 的值是:20
指针 ip 指向的地址是:0xbfc601ac
指针 ip 指向的地址存的变量的值是:20
空指针
赋为 NULL 值 的指针被称为空指针
cpp
#include <iostream>
using namespace std;
int main ()
{
int *ptr = NULL;
cout << "ptr 的值是 " << ptr ;
return 0;
}
运行结果:
ptr 的值是 0
内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
如需检查一个空指针,您可以使用 if 语句,如下所示:
scss
if(ptr) /* 如果 ptr 非空,则完成 */
if(!ptr) /* 如果 ptr 为空,则完成 */
因此,如果所有未使用的指针都被赋予空值,同时避免使用空指针,就可以防止误用一个未初始化的指针。很多时候,未初始化的变量存有一些垃圾值,导致程序难以调试。
指针自增自减比较
指针自增自减比较通常用于数组
指针递增
cpp
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr;
ptr = var; // 指针中的数组地址
// ptr = &var[MAX-1]; // 指针中最后一个元素的地址
for (int i = 0; i < MAX; i++)
{
cout << "Address of var[" << i << "] = ";
cout << ptr << endl;
cout << "Value of var[" << i << "] = ";
cout << *ptr << endl;
ptr++; // 移动到下一个位置
}
return 0;
}
运行结果:
css
Address of var[0] = 0xbfa088b0
Value of var[0] = 10
Address of var[1] = 0xbfa088b4
Value of var[1] = 100
Address of var[2] = 0xbfa088b8
Value of var[2] = 200
指针比较
cpp
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr;
ptr = var; // 指针中第一个元素的地址
int i = 0;
while ( ptr <= &var[MAX - 1] ) // 指针指向是否在数组范围内
{
cout << "Address of var[" << i << "] = ";
cout << ptr << endl;
cout << "Value of var[" << i << "] = ";
cout << *ptr << endl;
// 指向上一个位置
ptr++;
i++;
}
return 0;
}
运行结果:
css
Address of var[0] = 0xbfce42d0
Value of var[0] = 10
Address of var[1] = 0xbfce42d4
Value of var[1] = 100
Address of var[2] = 0xbfce42d8
Value of var[2] = 200
指针数组
在这里,把 ptr 声明为一个数组,由 MAX 个整数指针组成。因此,ptr 中的每个元素,都是一个指向 int 值的指针。下面的实例用到了三个整数,它们将存储在一个指针数组中,如下所示:
cpp
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr[MAX];
for (int i = 0; i < MAX; i++)
{
ptr[i] = &var[i]; // 赋值为整数的地址
}
for (int i = 0; i < MAX; i++)
{
cout << "Value of var[" << i << "] = ";
cout << *ptr[i] << endl;
}
return 0;
}
运行结果:
css
Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200
也可以用一个指向字符的指针数组来存储一个字符串列表,如下:
cpp
#include <iostream>
using namespace std;
const int MAX = 4;
int main ()
{
const char *names[MAX] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali",
};
for (int i = 0; i < MAX; i++)
{
cout << "Value of names[" << i << "] = ";
cout << names[i] << endl;
}
return 0;
}
运行结果:
css
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
指向指针的指针(多级间接寻址)
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链
指针的指针就是将指针的地址存放在另一个指针里面
通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置
一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。例如,下面声明了一个指向 int 类型指针的指针:
cpp
int **var;
当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:
cpp
#include <iostream>
using namespace std;
int main ()
{
int var;
int *ptr;
int **pptr;
var = 3000;
// 获取 var 的地址
ptr = &var;
// 使用运算符 & 获取 ptr 的地址
pptr = &ptr;
// 使用 pptr 获取值
cout << "var 值为 :" << var << endl;
cout << "*ptr 值为:" << *ptr << endl;
cout << "**pptr 值为:" << **pptr << endl;
return 0;
}
运行结果:
csharp
var 值为 :3000
*ptr 值为:3000
**pptr 值为:3000
多级间接寻址的应用场景
- 动态内存分配:使用多级指针可以实现动态内存分配,即分配一个指针数组,用于存储多个指针变量的地址,然后再分配每个指针变量所指向的内存空间。
- 多维数组:在多维数组中,可以使用多级指针来表示二维、三维等多维数组,以便在程序中动态地分配和访问多维数组。
- 数据结构:在数据结构中,可以使用多级指针来表示链表、树等数据结构,以便在程序中动态地添加、删除和修改节点。
- 函数指针:在函数指针中,可以使用多级指针来表示指向函数指针的指针,以便在程序中动态地管理函数指针
传递指针给函数
C++ 允许您传递指针给函数,只需要简单地声明函数参数为指针类型即可
下面的实例中,我们传递一个无符号的 long 型指针给函数,并在函数内改变这个值:
cpp
#include <iostream>
#include <ctime>
using namespace std;
// 在写函数时应习惯性的先声明函数,然后在定义函数
void getSeconds(unsigned long *par);
int main ()
{
unsigned long sec;
getSeconds( &sec );
cout << "Number of seconds :" << sec << endl // 输出实际值
return 0;
}
void getSeconds(unsigned long *par)
{
*par = time( NULL ); // 获取当前的秒数
return;
}
当上面的代码被编译和执行时,它会产生下列结果:
javascript
Number of seconds : 1695175690
能接受指针作为参数的函数,也能接受数组作为参数,如下所示:
cpp
#include <iostream>
using namespace std;
double getAverage(int *arr, int size); // 函数声明
int main ()
{
// 带有 5 个元素的整型数组
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
avg = getAverage( balance, 5 ); // 传递一个指向数组的指针作为参数
cout << "Average value is: " << avg << endl; // 输出返回值
return 0;
}
double getAverage(int *arr, int size)
{
int i, sum = 0;
double avg;
for (i = 0; i < size; ++i)
{
sum += arr[i];
}
avg = double(sum) / size;
return avg;
}
当上面的代码被编译和执行时,它会产生下列结果:
csharp
Average value is: 214.4
从函数返回指针
C++ 只支持在函数外返回 static 类型的局部变量的地址(因为如果不是 static 类型的变量,函数执行结束后该地址指向的局部变量就被销毁了,返回出来的这个指针就没有意义了)
现在,让我们来看下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们,具体如下:
cpp
#include <iostream>
#include <ctime>
#include <cstdlib>
using namespace std;
// 要生成和返回随机数的函数
int * getRandom( )
{
static int r[5];
srand( (unsigned)time( NULL ) ); // 设置种子
for (int i = 0; i < 5; ++i)
{
r[i] = rand();
cout << r[i] << endl;
}
return r;
}
// 要调用上面定义函数的主函数
int main ()
{
int *p; // 一个指向整数的指针
p = getRandom();
for ( int i = 0; i < 5; i++ )
{
cout << "*(p + " << i << ") : ";
cout << *(p + i) << endl;
}
return 0;
}
运行结果:
markdown
624723190
1468735695
807113585
976495677
613357504
*(p + 0) : 624723190
*(p + 1) : 1468735695
*(p + 2) : 807113585
*(p + 3) : 976495677
*(p + 4) : 613357504
数组
数组声明
必须声明数组元素类型、数组变量名、数组大小
cpp
type arrayName [ arraySize ];
如:
double balance[10];
数组初始化
采用花括号 {}
来包含数组
- 数组大小必须大于等于元素个数
- 数组大小不填则默认是元素个数
cpp
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
数组元素访问
cpp
double salary = balance[9];
多维数组
初始化
cpp
int a[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
访问
cpp
int val = a[2][3];
指向数组的指针
cpp
double *p;
double runoobAarray[10];
p = runoobAarray;
p = runoobAarray;
表示把第一个元素的地址 存储在 p 中,接下来就可以使用 *p
、*(p+1)
、*(p+2)
等来访问数组元素
cpp
#include <iostream>
using namespace std;
int main ()
{
// 带有 5 个元素的双精度浮点型数组
double runoobAarray[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
double *p;
p = runoobAarray;
// 输出数组中每个元素的值
cout << "使用指针的数组值 " << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "*(p + " << i << ") : ";
cout << *(p + i) << endl;
}
cout << "使用 runoobAarray 作为地址的数组值 " << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "*(runoobAarray + " << i << ") : ";
cout << *(runoobAarray + i) << endl;
}
return 0;
}
运行结果:
markdown
使用指针的数组值
*(p + 0) : 1000
*(p + 1) : 2
*(p + 2) : 3.4
*(p + 3) : 17
*(p + 4) : 50
使用 runoobAarray 作为地址的数组值
*(runoobAarray + 0) : 1000
*(runoobAarray + 1) : 2
*(runoobAarray + 2) : 3.4
*(runoobAarray + 3) : 17
*(runoobAarray + 4) : 50
传递数组给函数
三种函数形式参数的声明方式:
arduino
// 形式参数是一个指针:
void myFunction(int *param)
{
}
// 形式参数是一个已定义大小的数组:
void myFunction(int param[10])
{
}
// 形式参数是一个未定义大小的数组:
void myFunction(int param[])
{
}
例子:
cpp
#include <iostream>
using namespace std;
double getAverage(int arr[], int size); // 函数声明
int main ()
{
int balance[5] = {1000, 2, 3, 17, 50}; // 带有 5 个元素的整型数组
double avg;
avg = getAverage( balance, 5 ); // 传递一个指向数组的指针作为参数
cout << "平均值是:" << avg << endl; // 输出返回值
return 0;
}
double getAverage(int arr[], int size)
{
int i, sum = 0;
double avg;
for (i = 0; i < size; ++i)
{
sum += arr[i];
}
avg = double(sum) / size;
return avg;
}
从函数返回数组
由于在函数内部定义的数组属于局部变量,函数执行完后这个局部数组的内存会被释放,返回出来的指针指向的数组已经不存在了,所以不能用局部数组。只能选择用静态数组 或动态分配数组:
静态数组
静态数组就是前面加个 static
使用静态数组需要在函数内部创建一个静态数组,并将其地址返回,例如:
cpp
int* myFunction()
{
static int myArray[3] = {1, 2, 3};
return myArray;
}
让我们来看下面的函数,它会生成 10 个随机数,并使用数组来返回它们
cpp
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
// 要生成和返回随机数的函数
int * getRandom( )
{
static int r[5];
// 设置种子
srand( (unsigned)time( NULL ) );
for (int i = 0; i < 5; ++i)
{
r[i] = rand();
cout << r[i] << endl;
}
return r;
}
// 要调用上面定义函数的主函数
int main ()
{
// 一个指向整数的指针
int *p;
p = getRandom();
for ( int i = 0; i < 5; i++ )
{
cout << "*(p + " << i << ") : ";
cout << *(p + i) << endl;
}
return 0;
}
运行结果:
markdown
624723190
1468735695
807113585
976495677
613357504
*(p + 0) : 624723190
*(p + 1) : 1468735695
*(p + 2) : 807113585
*(p + 3) : 976495677
*(p + 4) : 613357504
动态分配数组
在函数内部动态分配的数组在函数执行结束时不会自动释放 ,所以需要调用函数的代码负责释放返回的数组
cpp
#include <iostream>
using namespace std;
int* createArray(int size) {
int* arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i + 1;
}
return arr;
}
int main() {
int* myArray = createArray(5); // 调用返回数组的函数,得到一个指向数组的指针
for (int i = 0; i < 5; i++) {
cout << myArray[i] << " ";
}
cout << endl;
delete[] myArray; // 释放内存
return 0;
}
运行结果:
1 2 3 4 5
引用
引用变量是一个已存在变量 的别名
引用 VS 指针
三个主要的不同:
- 不存在空引用;不需要判断空指针
- 不能修改指向,一旦引用被初始化为一个对象,就不能被指向到另一个对象;指针可以在任何时候指向到另一个对象
- 引用必须在创建时被初始化;指针可以在任何时间被初始化
创建引用
cpp
int i = 17;
int& r = i; // 成功创建 i 的引用变量 r
r 可以读作是"一个初始化为 i 的整型引用"
cpp
#include <iostream>
using namespace std;
int main ()
{
// 声明简单的变量
int i;
double d;
// 声明引用变量
int& r = i;
double& s = d;
i = 5;
cout << "Value of i : " << i << endl;
cout << "Value of i reference : " << r << endl;
d = 11.7;
cout << "Value of d : " << d << endl;
cout << "Value of d reference : " << s << endl;
return 0;
}
运行结果:
yaml
Value of i : 5
Value of i reference : 5
Value of d : 11.7
Value of d reference : 11.7
把引用作为参数
cpp
#include <iostream>
using namespace std;
// 函数声明
void swap(int& x, int& y);
int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
/* 调用函数来交换值 */
swap(a, b);
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
// 函数定义
void swap(int& x, int& y)
{
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}
把引用作为返回值
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。
当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边。例如,请看下面这个简单的程序:
cpp
#include <iostream>
using namespace std;
double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0};
double& setValues(int i) {
double& ref = vals[i];
return ref; // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i]
}
// 要调用上面定义函数的主函数
int main ()
{
cout << "改变前的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
setValues(1) = 20.23; // 改变第 2 个元素
setValues(3) = 70.8; // 改变第 4 个元素
cout << "改变后的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
return 0;
}
运行结果:
css
改变前的值
vals[0] = 10.1
vals[1] = 12.6
vals[2] = 33.1
vals[3] = 24.1
vals[4] = 50
改变后的值
vals[0] = 10.1
vals[1] = 20.23
vals[2] = 33.1
vals[3] = 70.8
vals[4] = 50
当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。
cpp
int& func() {
int q;
//! return q; // 在编译时发生错误
static int x;
return x; // 安全,x 在函数作用域外依然是有效的
}