C++总结(二)——C++数据基础

本文介绍了C++中的数据类型,包括基本类型、类型修饰符,并详细讨论了常量的使用,如const、volatile、mutable和register关键字。此外,还阐述了内存管理中的栈、堆、全局静态区、常量区、代码区和自由存储区的概念。
摘要由CSDN通过智能技术生成
C++总结(二)——C++数据基础

简述

在C++总结(一)中写了C++中的关键字,在后续的篇章中将会囊括C++几乎所有的知识点:比如:C++基础、类与对象、C++继承与派生、C++多态与虚函数、C++模板、C++重载、C++的STL库、C++输入输出流、C++文件操作、C++协程、C++异常处理等内容

数据

C++对数据的处理,最直观的感受就是讲某个数据放到栈中,交给CPU进行计算。这个过程中就要涉及数据该如何存储、数据的计算。为此需要对C++操作的数据进行分析。

C++数据存储

有什么数据

数据大体有两种:常量、变量

常量

含义:常量是指一旦被定义后,其值就不能被修改的数据。常量在程序中通常用来表示固定的数值或者字符串,以避免在程序运行过程中被误修改。

C++ 中常量的定义有两种方式:使用 #define 预处理命令或者使用 const 关键字。例如:

#define PI 3.14159
const int MAX_NUM = 100;

C++中常量的应用场景

  • 定义常量来表示一些固定的数值,如数学常数π、e等:
const double pi = 3.14159265358979323846;
const double e = 2.71828182845904523536;
  • 定义常量来表示一些固定的字符串,如程序中的提示信息、错误信息等。
const std::string error_message = "An error occurred.";
const std::string welcome_message = "Welcome to my program!";
  • 定义常量来表示一些固定的枚举值,如表示星期的枚举类型:
enum class Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday };
const Weekday first_day_of_week = Weekday::Monday;
  • 定义常量来表示一些固定的数组大小,如数组大小为10:
const int array_size = 10;
int array[array_size];
  • 函数常量:用于定义一些固定的函数,或者宏定义
  例如const int max(int a, int b)表示max函数返回的值是一个常量。
  #define LED_GPIO_0    1 

变量

含义:与常量不可修改的性质不同,变量就是可以修改的数据。C++中的变量是用于存储值的内存位置,它们具有标识符和数据类型。标识符是变量的名称,用于在程序中引用变量。数据类型指定变量可以存储的值的类型和范围。

应用示例

int age = 25;            // 定义一个名为age的整数类型变量,并将其初始化为25
double salary = 5000.50; // 定义一个名为salary的双精度浮点型变量,并将其初始化为5000.50
char grade = 'A';        // 定义一个名为grade的字符类型变量,并将其初始化为'A'
bool is_student = true;  // 定义一个名为is_student的布尔类型变量,并将其初始化为true

扩展:C++ 中的标识符是用来标识变量、函数、类、结构体、枚举、命名空间等程序元素的名称。标识符必须以字母或下划线开头,后面可以是字母、数字或下划线的组合,长度不限。标识符区分大小写,即大小写不同的标识符被视为不同的标识符。在 C++ 中,还有一些保留字不能作为标识符,如 if、else、while、switch 等等。标识符的命名应该有意义、简洁明了,并且遵循一定的命名规范,比如使用驼峰命名法等。简单例子如下

int age = 25 ; //age 是变量的标识符,int是变量age的数据类型

注意事项:标识符内不允许出现标点字符,比如 @、& 和 %。C++ 是区分大小写的编程语言

总结:

不用点:常量一般都会被const来修饰或者使用宏定义,而变量没有。本文后面会介绍const的属性

相同用点:除了宏定义,一般都需要说明数据的类型。比如int 、doubule、float.因此,接下来要分析数据的类型。


数据类型

为什么要给数据加上数据类型

C++需要数据类型是因为不同的数据类型在内存中占用的空间大小、存储方式、操作方式都不同,程序需要根据数据类型的特点来处理数据。

比如:我需要计算机给我输出一个字母 “A”,CPU需要在内存中找到这个字符,然后命令显示器显示一个"A"字符。内存中空间是很宝贵的。需要多少空间来存储这个A?一般是8位

char ager = 'A'

如上代码 char,就是说这个字符’A’的占位符是agre,在内存中需要占据8位置,就是1字节空间。

基本类型

七种基本的C++数据类型:

bool、char、int、float、double、void、wchar_t.

类型修饰符:

signed、unsigned、short、long

一些基本类型可以使用一个或多个类型修饰符进行修饰,比如:signed short int简写为short、signed long int 简写为long。

下面列举了C++常见的数据类型

数据类型范围占用的字节数
booltrue/false1
char-128 to 127 or 0 to 2551
short int-32,768 to 32,7672
int-2,147,483,648 to 2,147,483,6474
long int-2,147,483,648 to 2,147,483,6474
long long int-9,223,372,036,854,775,808 to 9,223,372,036,854,775,8078
float1.2E-38 to 3.4E+384
double2.3E-308 to 1.7E+3088
long double3.4E-4932 to 1.1E+493216
wchar_t1 wide character2 or 4 (depending on implementation)
void0

数据类型在不同系统中所占空间大小不同

数据类型8位操作系统16位操作系统32位操作系统64位操作系统
bool1111
char1111
unsigned char1111
short2222
unsigned short2222
int2244
unsigned int2244
long4448
unsigned long4448
long long8888
unsigned long long8888
float4444
double8888
long double881216

数据运算

有了数据类型,CPU就会对相应的数据进行运算。

常见的数据运算有

  • 算术运算:加减乘除、取模、自增自减等;
  • 比较运算:大于、小于、等于、不等于等;
  • 逻辑运算:与、或、非等;
  • 位运算:按位与、按位或、按位异或等;
  • 赋值运算:赋值、复合赋值等;
  • 条件运算:三目运算符等;
  • 类型转换运算:强制类型转换、隐式类型转换等。

算术运算

加法

加法运算用于将两个数值相加,C++中使用"+"符号表示,例如:

int a = 5;
int b = 3;
int c = a + b; // c的值为8

减法

减法运算用于将两个数值相减,C++中使用"-"符号表示,例如:

int a = 5;
int b = 3;
int c = a - b; // c的值为2

乘法

乘法运算用于将两个数值相乘,C++中使用"*"符号表示,例如:

int a = 5;
int b = 3;
int c = a * b; // c的值为15

除法

除法运算用于将两个数值相除,C++中使用"/"符号表示,例如:

int a = 5;
int b = 3;
int c = a / b; // c的值为1

注意:整数除法会向下取整,即舍去小数部分。

取模[其余]

取模运算用于求两个数值相除的余数,C++中使用"%"符号表示,例如:

int a = 5;
int b = 3;
int c = a % b; // c的值为2

自增自减运算

自增自减运算用于将一个数值加1或减1,C++中使用"++“和”–"符号表示,分为前置和后置两种形式。前置形式先进行加1或减1运算,再进行赋值操作;后置形式先进行赋值操作,再进行加1或减1运算。例如:

int a = 5;
a++; // 相当于a = a + 1;
int b = 3;
++b; // 相当于b = b + 1;
int c = 5;
c--; // 相当于c = c - 1;
int d = 3;
--d; // 相当于d = d - 1;

比较运算

大于

  int temp;
  int a = 5;
  int b = 6;
  if(a>b)
  {
    temp = a;
    a = b;
    b = temp;
  }
  

小于

  int temp;
  int a = 5;
  int b = 6;
  if(a<b)
  {
    temp = a;
    a = b;
    b = temp;
  }
  

等于

  int temp;
  int a = 5;
  int b = 6;
  if(a==b)
  {
    temp = a;
    a = b;
    b = temp;
  }
  

不等于

  int temp;
  int a = 5;
  int b = 6;
  if(a!=b)
  {
    temp = a;
    a = b;
    b = temp;
  }
  

逻辑运算

逻辑与运算符(&&):当两个操作数都为true时,结果为true,否则为false。

示例代码:

int a = 5;
int b = 10;
if (a > 0 && b > 0) {
    cout << "Both a and b are positive.";
}

逻辑或运算符(||):当两个操作数中至少有一个为true时,结果为true,否则为false。

int a = 5;
int b = -10;
if (a > 0 || b > 0) {
    cout << "At least one of a and b is positive.";
}

逻辑非运算符(!):将操作数的值取反,如果原来为true则变为false,反之亦然。

bool isTrue = true;
if (!isTrue) {
    cout << "This statement will not be executed.";
} else {
    cout << "This statement will be executed.";
}

逻辑异或运算符(^):当两个操作数不同时,结果为true,否则为false。

int a = 5;
int b = 10;
if (a > 0 ^ b > 0) 
{
    cout << "One of a and b is positive, but not both.";
}

位运算

按位与运算符(&):两个二进制数对应位都为1,结果才为1,否则为0。

int a = 5;       // 二进制表示为 0101
int b = 3;       // 二进制表示为 0011
int c = a & b;   // 二进制表示为 0001,结果为1

按位或运算符(|):两个二进制数对应位只要有一个为1,结果就为1,否则为0。

int a = 5;       // 二进制表示为 0101
int b = 3;       // 二进制表示为 0011
int c = a | b;   // 二进制表示为 0111,结果为7

按位异或运算符(^):两个二进制数对应位不同,结果为1,否则为0。

int a = 5;       // 二进制表示为 0101
int b = 3;       // 二进制表示为 0011
int c = a ^ b;   // 二进制表示为 0110,结果为6

按位取反运算符(~):对一个二进制数的每一位取反。

int a = 5;       // 二进制表示为 0101
int b = ~a;      // 二进制表示为 1010,结果为-6

左移运算符:将一个二进制数向左移动指定位数,右边补0。

int a = 5;       // 二进制表示为 0101
int b = a << 2;  // 二进制表示为 010100,结果为20

右移运算符(>>):将一个二进制数向右移动指定位数,左边补符号位。

int a = 5;       // 二进制表示为 0101
int b = a >> 2;  // 二进制表示为 0001,结果为1

赋值运算

简单赋值运算符(=):将右侧表达式的值赋给左侧变量。

int a = 10;
int b = a;

复合赋值运算符:将右侧表达式的值和左侧变量的值进行指定的运算,并将结果赋给左侧变量。

int a = 5;
a += 10; // a = a + 10;
a -= 5; // a = a - 5;
a *= 2; // a = a * 2;
a /= 3; // a = a / 3;
a %= 2; // a = a % 2;

位运算赋值运算符:将右侧表达式的值和左侧变量的值进行位运算,并将结果赋给左侧变量。

int a = 10;
a &= 6; // a = a & 6;
a |= 12; // a = a | 12;
a ^= 3; // a = a ^ 3;
a <<= 2; // a = a << 2;
a >>= 1; // a = a >> 1;

自增自减运算符:将变量的值自增或自减1,并将结果赋给变量。

int a = 5;
a++; // a = a + 1;
a--; // a = a - 1;

逗号运算符:将逗号分隔的多个表达式依次执行,并将最后一个表达式的值赋给左侧变量。

int a = 1, b = 2, c = 3;
a = (b++, c++, b+c); // a = 5, b = 3, c = 4

条件运算

C++中的条件运算符是?、:,也被称为三目运算符,它的语法如下:

条件 ? 如果条件为真则执行此处代码 : 如果条件为假则执行此处代码;
int a = 10;
int b = 20;
int max = a > b ? a : b;

在这个例子中,如果a大于b,则max被赋值为a,否则max被赋值为b。

除了三目运算符,C++中还有一些其他的条件运算符,包括:

  • 逻辑与运算符(&&):如果两个操作数都是true,则返回true,否则返回false。
  • 逻辑或运算符(||):如果两个操作数中的任意一个是true,则返回true,否则返回false。
  • 逻辑非运算符(!):如果操作数为true,则返回false,否则返回true。
int a = 10;
int b = 20;
bool result1 = (a > b) && (a != b);
bool result2 = (a < b) || (a == b);
bool result3 = !(a > b);

cout << result1 << endl; // 输出0,因为a不大于b
cout << result2 << endl; // 输出1,因为a小于b
cout << result3 << endl; // 输出1,因为a不大于b

类型转换运算

C++中的类型转换运算有四种:

隐式类型转换:在程序中自动进行的类型转换,不需要程序员手动干预。

int a = 10;
double b = a; // 隐式将int类型的a转换为double类型的b

显式转换:通过强制类型转换符进行的类型转换,需要程序员手动干预。

double b = 3.14;
int a = (int)b; // 显式将double类型的b转换为int类型的a

C风格的类型转换:使用C语言中的强制类型转换方式进行类型转换。

double b = 3.14;
int a = (int)b; // C风格的强制类型转换

函数式类型转换:使用特殊的类型转换函数进行类型转换。

double b = 3.14;
int a = static_cast<int>(b); // 函数式的类型转换

其中,函数式类型转换是最安全的类型转换方式,也是C++推荐的类型转换方式。

double b = 3.14;
int a = static_cast<int>(b); // 使用static_cast进行函数式类型转换

修饰数据的关键字

const

const:用于定义常量,表示变量的值不能被修改。

C++ 中的 const 关键字用于指定一个变量或对象为常量,其值不能被修改。const 可以应用于变量、函数返回类型、函数参数、成员函数等等,具体如下:

常量变量

const int num = 10; // 定义常量变量

常量变量在声明时必须初始化,并且一旦初始化之后,就不能再修改其值。

常量指针

const int *p = &num; // 定义指向常量的指针

const 可以放在指针类型前面,表示指针所指向的值为常量,即不能通过指针修改所指向的值。

指向常量的常量指针

const int * const p = &num; // 定义指向常量的常量指针

指向常量的常量指针既不能修改指针本身,也不能通过指针修改所指向的值。

常量引用

const int &ref = num; // 定义常量引用

常量引用可以绑定到常量或非常量对象上,但无论引用的对象是否为常量,都不能通过引用修改其值。

常量成员函数

class Foo {
public:
    int getValue() const { // 声明常量成员函数
        return value;
    }
private:
    int value;
};

常量成员函数表示该函数不会修改成员变量的值,其声明方式为在函数后加上 const 关键字。

volatile

volatile:用于修饰变量,表示该变量的值可能在程序执行期间被改变,通常用于多线程编程和操作硬件设备。

提醒编译器不要对变量的读写操作进行优化。

volatile int *device_address = (int *)0x1000; // 假设 0x1000 是某个硬件设备的地址
*device_address = 0x1234; // 向硬件设备写入数据
int value = *device_address; // 从硬件设备读取数据

这里将指针 device_address 声明为 volatile int * 类型,告诉编译器不要对读写操作进行优化,保证每次访问的都是设备的最新值。

多线程环境下变量的可见性

volatile bool flag = false; // 信号量

void thread1() {
    while (!flag) {} // 等待 flag 变为 true
    // do something
}

void thread2() {
    // do something
    flag = true; // 修改 flag 的值
}

在这个例子中,flag 变量被声明为 volatile bool 类型,保证在多线程环境下对该变量的读写操作是可见的。线程1中的循环中读取的是 flag 的最新值,线程2中修改的 flag 的值也能够被线程1看到。

注意事项

volatile 并不能完全保证多线程环境下变量的可见性,还需要结合其它机制(例如使用原子操作或互斥锁)来保证线程安全。

static

static:用于修饰全局变量或者函数,表示它们在内存中只有一份拷贝,不会被其他文件所访问。

函数内 static 变量

void func() {
  static int count = 0;
  count++;
  cout << "count: " << count << endl;
}

int main() {
  func(); // count: 1
  func(); // count: 2
  func(); // count: 3
  return 0;
}

在函数内部定义的 static 变量会在函数第一次被调用时被初始化,然后在后续调用中保留其值,直到程序结束。因此,在上面的示例中,count 的值会依次为 1、2、3。

文件内 static 变量

static int count = 0;

void func1() {
  count++;
}

void func2() {
  cout << "count: " << count << endl;
}

int main() {
  func1();
  func2(); // count: 1
  func1();
  func2(); // count: 2
  return 0;
}

在文件内定义的 static 变量只能在当前文件中访问。在上面的示例中,count 的初始值为 0,然后在函数 func1 中递增,最后在函数 func2 中输出。

类内 static 成员变量

class MyClass {
public:
  static int count;
  MyClass() { count++; }
};

int MyClass::count = 0;

int main() {
  MyClass obj1;
  MyClass obj2;
  cout << "count: " << MyClass::count << endl; // count: 2
  return 0;
}

在类中定义的 static 成员变量只有一份,与类的实例无关。在上面的示例中,count 初始值为 0,在创建 MyClass 类型的对象时会递增,最后输出 count 的值为 2。

类内 static 成员函数

class MyClass {
public:
  static int getCount() {
    return count;
  }
private:
  static int count;
};

int MyClass::count = 0;

int main() {
  cout << "count: " << MyClass::getCount() << endl; // count: 0
  MyClass obj1;
  MyClass obj2;
  cout << "count: " << MyClass::getCount() << endl; // count: 2
  return 0;
}

在类中定义的 static 成员函数没有 this 指针,只能访问类的静态成员变量。在上面的示例中,getCount 函数返回类的静态成员变量 count 的值。

在 C++ 中,extern 关键字用于说明一个变量或函数的声明是“外部链接的”,即在其他文件中定义的,而不是在当前文件中定义的。extern 可以用于变量和函数

extern 变量

// file1.cpp
#include <iostream>
extern int g_var;

void func() {
  std::cout << "g_var: " << g_var << std::endl;
}

// file2.cpp
int g_var = 42;

int main() {
  func(); // g_var: 42
  return 0;
}

在 file1.cpp 文件中,使用 extern 关键字声明了一个名为 g_var 的变量,这意味着该变量在其他文件中定义,而不是在当前文件中定义。然后在 func 函数中使用了 g_var 变量。在 file2.cpp 文件中,定义了 g_var 变量并初始化为 42。在 main 函数中调用了 func 函数,输出了 g_var 变量的值 42。

extern 函数

// file1.cpp
#include <iostream>
extern void func();

int main() {
  func();
  return 0;
}

// file2.cpp
#include <iostream>

void func() {
  std::cout << "Hello, World!" << std::endl;
}


在 file1.cpp 文件中,使用 extern 关键字声明了一个名为 func 的函数,这意味着该函数在其他文件中定义,而不是在当前文件中定义。然后在 main 函数中调用了 func 函数。在 file2.cpp 文件中,定义了 func 函数并输出了一条信息。最终运行程序会输出 Hello, World!。

注意事项

extern 变量或函数的声明只是告诉编译器该变量或函数的定义在其他文件中,具体的定义还需要在其他文件中进行。因此,在使用 extern 的时候,需要确保对应的定义已经存在,并且在编译时需要将多个文件进行链接。

auto

auto:用于变量声明中,让编译器自动推断变量的类型。

auto 推导基本类型变量

auto i = 42;         // 推导为 int 类型
auto d = 3.14;       // 推导为 double 类型
auto c = 'c';        // 推导为 char 类型
auto b = true;       // 推导为 bool 类型
auto s = "hello";    // 推导为 const char* 类型

在这个例子中,编译器会根据变量初始化表达式的类型自动推导出变量的类型。

auto 推导模板类型变量

#include <vector>
#include <iostream>

int main() {
  std::vector<int> v{1, 2, 3};
  auto it = v.begin();
  std::cout << *it << std::endl;  // 输出 1
  return 0;
}

在这个例子中,我们使用 auto 关键字来推导 it 变量的类型,这个变量是一个指向 vector 容器中的第一个元素的迭代器,它的类型实际上是 std::vector::iterator 类型,但是使用 auto 关键字可以让编译器自动推导出类型,使得代码更加简洁。

auto 推导 lambda 表达式类型

  #include <iostream>

int main() {
  auto func = [](int x, int y) { return x + y; };
  std::cout << func(1, 2) << std::endl;  // 输出 3
  return 0;
}

在这个例子中,我们使用 auto 关键字来推导 func 变量的类型,这个变量是一个 lambda 表达式,它的类型是一个未命名的函数类型,但是使用 auto 关键字可以让编译器自动推导出类型。

注意事项

使用 auto 关键字推导的变量类型必须能够在编译时确定。同时,auto 推导出的类型和变量初始化表达式的类型并不完全一致,例如,auto 推导出的容器类型是带有类型信息的,而不是一个裸指针类型。

register:用于修饰局部变量,表示该变量被频繁访问,可以被编译器存储在 CPU 的寄存器中,以提高程序运行速度。在早期的 C 和 C++ 中,register 关键字用于提示编译器将变量存储在 CPU 寄存器中,以提高程序的性能。但是现代的编译器已经具有非常强大的优化能力,能够自动地将变量存储在寄存器中,所以 register 关键字已经没有必要了。在现代的 C++ 中,如果需要优化程序的性能,可以使用更加先进的技术,例如使用更高级的数据结构、使用并行计算、使用 GPU 等。

mutable

mutable:用于修饰类的成员变量,表示该变量可以在 const 函数中被修改。

mutable 修饰计数器

class Counter {
public:
Counter() : count(0) {}

void increment() const {
  ++count; // count 被 mutable 修饰,即使在 const 函数中也可以修改它的值
}

int getCount() const {
  return count;
}

private:
mutable int count;
};

在这个例子中,Counter 类有一个计数器 count,它被 mutable 修饰,因此即使在 const 成员函数 increment() 中也可以对它进行修改,这样可以方便地在 const 成员函数中统计某些信息。

mutable 修饰缓存

class Cache {
public:
int get(int key) const {
  auto it = cache.find(key);
  if (it != cache.end()) {
    // 返回缓存中的值,同时更新计数器
    ++(it->second.accessCount);
    return it->second.value;
  } else {
    // 如果缓存中没有该值,则计算出该值并将其加入缓存
    int value = calculateValue(key);
    cache[key] = {value, 1};
    return value;
  }
}

private:
struct CacheEntry {
  int value;
  mutable int accessCount; // accessCount 被 mutable 修饰,即使在 const 函数中也可以修改它的值
};

int calculateValue(int key) const {
  // 计算缓存中没有的值
}

std::unordered_map<int, CacheEntry> cache;
};
 

在这个例子中,Cache 类是一个缓存,它存储了一些键值对,其中每个值都有一个访问计数器 accessCount。当使用 get() 函数获取某个键对应的值时,如果该键已经存在于缓存中,则返回该值并将其对应的计数器加 1,否则计算出该值并将其加入缓存。在这个过程中,访问计数器 accessCount 被 mutable 修饰,即使在 const 函数中也可以修改它的值,这样可以方便地统计每个值的访问次数。

注意事项

mutable 关键字虽然可以让被修饰的变量在 const 成员函数中被修改,但是这并不意味着应该在 const 成员函数中频繁地修改这些变量,因为这可能会导致程序逻辑上的混乱和不可预测性。一般来说,mutable 关键字应该只用于修饰那些被认为不会影响类的逻辑一致性

explicit

explicit:用于声明类的构造函数或转换函数,防止编译器进行隐式类型转换。

防止隐式类型转换

class MyClass {
public:
explicit MyClass(int value) : value(value) {}

int getValue() const {
  return value;
}

private:
int value;
};

void foo(MyClass obj) {
// ...
}

int main() {
foo(42); // 错误:不能将 int 类型隐式转换为 MyClass 类型
foo(MyClass(42)); // 正确
}

在这个例子中,MyClass 类有一个单参数构造函数,它被 explicit 修饰,因此编译器不会自动进行隐式类型转换。在 main() 函数中,尝试将整数 42 作为参数传递给函数 foo(),但是由于不能将 int 类型隐式转换为 MyClass 类型,因此会导致编译错误。需要使用显式类型转换,将整数 42 转换为 MyClass 类型的对象

防止多次类型转换

 class Complex {
public:
  Complex(double real, double imag) : real(real), imag(imag) {}
  explicit Complex(double value) : real(value), imag(0) {}

  double getReal() const {
    return real;
  }

  double getImag() const {
    return imag;
  }

private:
  double real;
  double imag;
};

void foo(const Complex& c) {
  // ...
}

int main() {
  foo(3.14); // 错误:不能将 double 类型隐式转换为 Complex 类型
  foo(Complex(3.14)); // 正确
}
 

在这个例子中,Complex 类有两个构造函数,一个是普通的两参数构造函数,另一个是单参数构造函数。如果单参数构造函数没有被 explicit 修饰,那么当使用一个 double 类型的实参调用 foo() 函数时,编译器会先将实参隐式转换为 Complex 类型的对象,然后再将该对象传递给 foo() 函数,这样就会产生多次类型转换的开销和可能的错误。由于单参数构造函数被 explicit 修饰,因此编译器不能自动进行类型转换,需要使用显式类型转换,将 double 类型的实参转换为 Complex 类型的对象

注意事项

explicit

explicit 关键字只能用于单参数构造函数和转换函数,而不能用于其他类型的函数。另外,如果一个构造函数同时也是默认参数函数,那么不能将其标记为 explicit,否则将无法使用该构造函数作为默认参数函数。

声明常量表达式变量

constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
constexpr int N = 5;
int arr[factorial(N)]; // 编译时确定数组大小
// ...
}

在这个例子中,使用 constexpr 声明了一个递归函数 factorial(),该函数可以在编译时求出参数 n 的阶乘。在 main() 函数中,使用 constexpr 声明了一个常量表达式变量 N,并将其作为参数传递给 factorial() 函数,计算出数组的大小。由于 N 是一个编译时常量,因此编译器可以在编译时计算出数组的大小,从而避免了运行时开销

声明常量表达式函数

  constexpr double pi = 3.14159265358979323846;

constexpr double area(double radius) {
  return pi * radius * radius;
}

int main() {
  constexpr double r = 1.0;
  double a = area(r); // 编译时求值
  // ...
}

使用 constexpr 声明了一个常量表达式变量 pi,表示圆周率。另外,使用 constexpr 声明了一个函数 area(),用于计算圆的面积。在 main() 函数中,使用 constexpr 声明了一个常量表达式变量 r,并将其作为参数传递给 area() 函数,计算出圆的面积。由于 area() 函数是一个常量表达式函数,因此编译器可以在编译时对其进行求值,从而避免了运行时开销。

声明常量表达式构造函数

  class Point {
  public:
  constexpr Point(int x, int y) : x(x), y(y) {}

  int getX() const {
    return x;
  }

  int getY() const {
    return y;
  }

  private:
  int x;
  int y;
  };

  int main() {
  constexpr Point p(1, 2); // 编译时求值
  int x = p.getX(); // 运行时读取值
  // ...
}

使用 constexpr 声明了一个构造函数 Point(),用于初始化一个点的坐标。在 main() 函数中,使用 constexpr 声明了一个常量表达式变量 p,并将其作为参数传递给 Point() 构造函数,创建一个常量表

inline

inline:用于修饰函数,表示该函数可以在调用处被直接展开,以提高程序运行速度。

声明内联函数

 inline int square(int x) {
  return x * x;
}

int main() {
  int a = 5;
  int b = square(a); // 直接展开函数
  // ...
}
 

使用 inline 声明了一个求平方的内联函数 square(),在 main() 函数中调用该函数时,编译器会将函数展开为 b = a * a;,从而避免了函数调用的开销

声明内联类成员函数

 class Point {
public:
  inline double distance(const Point& p) const {
    double dx = x - p.x;
    double dy = y - p.y;
    return sqrt(dx * dx + dy * dy);
  }

private:
  double x;
  double y;
};

int main() {
  Point p1(1.0, 2.0);
  Point p2(3.0, 4.0);
  double d = p1.distance(p2); // 直接展开函数
  // ...
}
 

在这个例子中,使用 inline 声明了一个计算两点距离的内联成员函数 distance(),在 main() 函数中调用该函数时,编译器会将函数展开为 d = p1.distance(p2) = sqrt((1.0 - 3.0) * (1.0 - 3.0) + (2.0 - 4.0) * (2.0 - 4.0));,从而避免了函数调用的开销。

声明内联模板函数

template <typename T>
inline T max(T a, T b) {
  return a > b ? a : b;
}

int main() {
  int x = 1, y = 2;
  int z = max(x, y); // 直接展开函数
  // ...
}

使用 inline 声明了一个求最大值的内联模板函数 max(),在 main() 函数中调用该函数时,编译器会将函数展开为 z = max(x, y) = x > y ? x : y;,从而避免了函数调用的开销。由于 max() 是一个模板函数,因此编译器会根据参数类型自动推导出函数的实例化版本。

noexcept

noexcept:用于声明函数不会抛出异常,以提高程序的健壮性。

声明函数不会抛出异常

int foo() noexcept {
  // 函数体
}
//在这个例子中,使用 noexcept 关键字声明函数 foo() 不会抛出异常。

声明函数不会抛出异常

  int bar() noexcept(false) {
  // 函数体
}
//在这个例子中,使用 noexcept(false) 表示函数 bar() 可能会抛出异常。

声明函数不会抛出异常

 class MyClass {
public:
  ~MyClass() noexcept {
    // 异常安全的清理工作
  }
};

void func(MyClass obj) noexcept {
  // 函数体
}
 

类 MyClass 的析构函数使用 noexcept 表示在清理工作中不会抛出异常。函数 func() 的参数是一个 MyClass 类型的对象,该对象在函数返回时会被销毁,因此如果 MyClass 的析构函数抛出异常,那么程序就会在异常处理过程中终止。为了确保程序能够正常退出,这里使用 noexcept 来保证在调用析构函数时不会抛出异常。

使用 noexcept 来优化代码

int divide(int a, int b) noexcept {
  if (b == 0) {
    throw std::logic_error("divide by zero");
  }
  return a / b;
}

int main() {
  int a = 10;
  int b = 2;
  int c = 0;
  try {
    c = divide(a, b);
  } catch (const std::exception& e) {
    // 处理异常
  }
  // ...
}

在这个例子中,函数 divide() 可能会抛出异常,因此在 main() 函数中对其进行了异常处理。但是,如果我们知道 divide() 不会抛出异常,那么就可以使用 noexcept 来优化代码:

int divide(int a, int b) noexcept {
  // 省略异常处理代码
  return a / b;
}

int main() {
  int a = 10;
  int b = 2;
  int c = divide(a, b);
  // ...
}

由于 divide() 不会抛出异常,因此可以将异常处理代码省略掉,从而提高程序的性能。

在模板函数中使用 noexcept

template <typename T>
void mySwap(T& a, T& b) noexcept(noexcept(a.swap(b))) {
  a.swap(b);
}

int main() {
  std::string s1("hello");
  std::string s2("world");
  mySwap(s1, s2);
  // ...
}

我们定义了一个模板函数 mySwap(),它接受两个参数 a 和 b,并在函数体中调用它们的 swap() 函数。我们使用 noexcept 关键字来指定 mySwap() 函数是否可能会抛出异常。在这个例子中,noexcept 的参数是 a.swap(b),也就是调用 a 和 b 的 swap() 函数。由于 std::string 类型的 swap() 函数不会抛出异常,因此可以使用 noexcept(a.swap(b)) 来表示 mySwap() 不会抛出异常。

thread_local

thread_local:用于声明线程局部变量,表示该变量在每个线程中有独立的实例。

线程局部计数器

#include <iostream>
#include <thread>

thread_local int count = 0;

void foo() {
  std::cout << "Thread " << std::this_thread::get_id() << " count: " << ++count << std::endl;
}

int main() {
  std::thread t1(foo);
  std::thread t2(foo);
  t1.join();
  t2.join();
  return 0;
}  

他们定义了一个 thread_local 变量 count,并在每个线程中调用函数 foo() 来对其进行自增操作。由于 count 是线程局部变量,所以不同线程中的 count 变量互不干扰,输出结果如下:

Thread 139736123938048 count: 1
Thread 139736115545344 count: 1

单例模式的线程安全实现

#include <iostream>
#include <mutex>

class Singleton {
public:
  static Singleton& instance() {
    thread_local static Singleton singleton;
    return singleton;
  }

  void print() {
    std::cout << "Thread " << std::this_thread::get_id() << " singleton: " << this << std::endl;
  }

private:
  Singleton() {}
  ~Singleton() {}

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;
};

int main() {
  std::thread t1([](){
    Singleton& s = Singleton::instance();
    s.print();
  });
  std::thread t2([](){
    Singleton& s = Singleton::instance();
    s.print();
  });
  t1.join();
  t2.join();
  return 0;
}

在这个例子中,我们定义了一个线程安全的单例模式类 Singleton,其中 instance() 方法返回一个线程局部的 static 变量 singleton。这样就可以保证不同线程中调用 instance() 方法时返回的 Singleton 对象是独立的,从而实现了线程安全。输出结果如

Thread 139820920592128 singleton: 0x7f9d1c000000
Thread 139820912199424 singleton: 0x7f9d1c000000

注意事项

thread_local 关键字的作用是声明一个线程局部存储的变量,在多线程编程中有一定的应用价值。但是需要注意,在使用 thread_local 关键字时,需要注意不同线程中的变量互不干扰,需要谨慎使用。

alignas

alignas:用于声明变量的内存对齐方式,可以让变量在内存中占用更少的空间,提高程序的效率。

指定类型的对齐方式

alignas(16) struct alignas(16) myStruct
{
   char a;
   int b;
   short c;
};  

上面的示例指定myStruct类型的对齐方式为16字节,这意味着它的内存布局将会按照16字节对齐。在这种情况下,myStruct中的a成员将会占用1字节,然后需要对齐到16字节边界,所以会在其后面填充15个字节的空白。接着,b成员需要按照4字节对齐,因此在a成员后面填充3个字节的空白,然后b成员占用4字节。最后,c成员需要按照2字节对齐,所以在b成员后面填充2个字节的空白,然后c成员占用2字节。

指定变量的对齐方式

alignas(16) int alignas(16) myInt;

上面的示例指定myInt变量的对齐方式为16字节,这意味着它的地址应该是16的倍数。如果默认对齐方式小于16字节,那么在分配内存时会按照16字节对齐,从而增加一些额外的空间。

指定数组的对齐方式

alignas(32) int alignas(32) myArray[4];

上面的示例指定myArray数组的对齐方式为32字节。在这种情况下,数组的第一个元素将会按照32字节对齐,然后后续的元素也会按照相同的方式对齐。

指定函数指针的对齐方式

alignas(16) void (*myFunction)(int);

上面的示例指定myFunction函数指针的对齐方式为16字节。这意味着在分配内存时,要按照16字节对齐函数指针的地址。

alignof:用于获取变量的内存对齐方式。

可以使用 alignof 获取变量的对齐要求

#include <iostream>
using namespace std;

int main()
{
    alignas(16) int a;
    cout << alignof(decltype(a)) << endl; // 输出 16
    return 0;
}

在上面的代码中,我们定义了一个变量 a,并使用 alignas 将其对齐到 16 字节边界。然后使用 decltype(a) 获取 a 的类型,并将其作为参数传递给 alignof,以获取该类型的对齐要求。在这种情况下,输出将是 16。

对类型使用 alignof

#include <iostream>
using namespace std;

struct alignas(8) S {
    int x;
    char y;
    double z;
};

int main()
{
    cout << alignof(S) << endl; // 输出 8
    return 0;
}

在上面的代码中,我们定义了一个名为 S 的结构体,并使用 alignas 将其对齐到 8 字节边界。然后使用 alignof(S) 获取 S 的对齐要求。在这种情况下,输出将是 8。

对表达式使用 alignof

#include <iostream>
using namespace std;

int main()
{
    cout << alignof(decltype(1 + 2.0)) << endl; // 输出 8
    return 0;
}

在上面的代码中,我们使用 decltype 获取表达式 1 + 2.0 的类型,并将其作为参数传递给 alignof,以获取该类型的对齐要求。在这种情况下,输出将是 8,因为 1 + 2.0 的类型是 double,其对齐要求是 8 字节。

[[deprecated]]

[[deprecated]]:用于标记已经过时的函数或变量,编译器会在编译时给出警告信息。

标记函数为被弃用

[[deprecated("This function is deprecated. Use some_other_function instead.")]]
void old_function();

标记类的某个成员函数为被弃用

class MyClass {
public:
    [[deprecated("This function is deprecated. Use some_other_function instead.")]]
    void old_function();
};


标记整个类为被弃用

[[deprecated("This class is deprecated. Use some_other_class instead.")]]
class OldClass {};

使用 [[deprecated]] 属性和 #pragma 指令结合使用,以便编译器发出警告

[[deprecated("This function is deprecated. Use some_other_function instead.")]]
void old_function();

#pragma GCC warning "old_function is deprecated. Use some_other_function instead."
//在这个例子中,使用了 #pragma GCC warning 指令,以便编译器在编译时发出警告,提示程序员该函数已经被弃用。

注意事项

尽管 [[deprecated]] 属性可以帮助程序员识别出被弃用的代码,但它并不能自动替换这些代码。程序员需要手动将这些代码替换为新的代码,以便维护代码的可靠性和可维护性。

数据在内存中的分布

在C++中,内存被划分为以下几个区:

  • 栈(Stack):用于存储局部变量和函数参数等,由编译器自动分配和释放内存,具有先进后出的特性。
void foo() 
{
    int x = 10;
    char c = 'a';
    // ...
}

  • 堆(Heap):用于存储动态分配的内存,由程序员手动分配和释放,具有随机访问的特性。
int* ptr = new int[10];
delete[] ptr;

  • 全局静态区(Global static area):用于存储全局静态变量和静态成员变量,程序启动时分配内存,程序结束时释放内存。
static int x = 0;
class A 
{
    static int y;
    // ...
};

  • 常量区(Constant area):用于存储常量,例如字符串常量等,只读,无法修改。
const char* str = "Hello World";
  • 代码区(Code area):用于存储程序的可执行代码,只读,无法修改。
void foo() {
    // ...
}
  • 自由存储区(Free store):用于存储动态分配的内存,与堆类似,但是更加灵活,可以使用new和delete运算符动态分配和释放内存。
  • 内存映射文件区(Memory-mapped file area):用于将文件映射到内存中,使得程序可以像访问内存一样访问文件内容。
  • 栈顶空间(Stack top):用于存储栈顶指针以及函数调用过程中的临时变量等。
  • 空闲区(Free area):未被分配的内存区域,可以被重新分配给程序使用。

自由存储区

C++中的自由存储区指的是在程序运行期间动态分配的内存区域,由new和malloc等函数进行分配,由delete和free等函数进行释放。自由存储区分配的内存块大小可以动态变化,且分配的内存块大小可以大于栈的容量。

#include <iostream>
using namespace std;

int main() {
    int* p = new int; // 动态分配一个整型内存块
    *p = 10; // 给动态分配的内存块赋值
    cout << *p << endl; // 输出动态分配的内存块的值
    delete p; // 释放动态分配的内存块
    return 0;
}

上述代码中,使用new关键字动态分配了一个整型内存块,然后将值赋为10,最后使用delete关键字释放内存块。

除了使用new和delete进行内存分配和释放,还可以使用malloc和free函数实现动态内存分配,示例代码如下:

#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
    int* p = (int*)malloc(sizeof(int)); // 动态分配一个整型内存块
    *p = 10; // 给动态分配的内存块赋值
    cout << *p << endl; // 输出动态分配的内存块的值
    free(p); // 释放动态分配的内存块
    return 0;
}

上述代码中,使用malloc函数动态分配了一个整型内存块,然后将值赋为10,最后使用free函数释放内存块。需要注意的是,使用malloc函数分配内存时需要强制类型转换,因为malloc函数返回的是void*类型指针,需要将其转换为所需类型的指针。

内存映射文件区

C++ 中的内存映射文件区可以使用操作系统提供的相关函数进行创建和操作。以下是使用 POSIX API 创建和映射内存映射文件区的示例代码:

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    // 打开文件
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 获取文件大小
    struct stat st;
    if (fstat(fd, &st) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }
    size_t size = st.st_size;

    // 映射文件到内存
    void* addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 使用映射后的内存
    char* buffer = reinterpret_cast<char*>(addr);
    buffer[0] = 'H';
    buffer[1] = 'e';
    buffer[2] = 'l';
    buffer[3] = 'l';
    buffer[4] = 'o';
    buffer[5] = '\0';

    // 解除内存映射并关闭文件
    if (munmap(addr, size) == -1) {
        perror("munmap");
        close(fd);
        return 1;
    }
    close(fd);

    return 0;
}
  

该示例代码使用 open 函数打开文件,然后使用 fstat 函数获取文件大小。接下来,使用 mmap 函数将文件映射到内存中,并将返回的指针强制转换为字符指针类型。使用指针访问映射后的内存,最后使用 munmap 函数解除内存映射。

在 C++ 中,空闲区是指未被分配给任何对象或者数据的内存区域。由于 C++ 中内存的分配和释放都是由程序员控制的,因此可以通过动态分配内存的方式创建空闲区。

以下是一个简单的示例代码,演示了如何在 C++ 中使用 new 和 delete 运算符动态分配和释放内存,从而创建和释放空闲区:

#include <iostream>

int main() {
    int* ptr = new int;  // 分配一个整型数的内存
    *ptr = 123;  // 将值 123 存储到该内存中
    std::cout << "The value of the pointer is: " << *ptr << std::endl;

    delete ptr;  // 释放该内存

    return 0;
}

在这个示例中,我们使用 new 运算符动态分配一个 int 类型的内存区域,并将值 123 存储到该内存中。在程序运行完毕后,我们使用 delete 运算符将该内存释放,从而将其返回到空闲区。需要注意的是,在实际开发中,为了避免内存泄漏和其他问题,通常会使用智能指针等工具来管理内存。同时,为了提高内存分配和释放的效率,也可以使用内存池等技术来优化空闲区的使用。

智能指针

在C++中,智能指针是一种特殊的指针类型,可以自动管理动态分配的内存。智能指针通常被用来避免内存泄漏和悬空指针的问题,以及方便地共享对象的所有权。

C++标准库提供了两种智能指针:std::unique_ptr和std::shared_ptr。以下是它们的详细描述和代码例子:

std::unique_ptr

  • std::unique_ptr是一个独占式智能指针,它拥有被指向对象的唯一所有权。
  • std::unique_ptr被销毁时,它会自动删除它所指向的对象。该指针不能被复制,只能移动。
#include <memory>
#include <iostream>

int main() {
  std::unique_ptr<int> ptr(new int(42));
  std::cout << *ptr << std::endl; // 输出 42
  return 0;
}
  

在这个例子中,我们创建了一个std::unique_ptr,它指向一个动态分配的int类型对象。我们可以使用*运算符来访问该指针所指向的对象,并输出它的值。当main()函数结束时,ptr将被销毁,并自动释放它所拥有的内存。

std::shared_ptr

std::shared_ptr是一个共享式智能指针,可以被多个指针同时拥有。每当一个新的
std::shared_ptr指向一个对象时,它的引用计数就会增加。当所有指向该对象的
std::shared_ptr都被销毁时,该对象才会被自动删除。

#include <memory>
#include <iostream>

int main() {
  std::shared_ptr<int> ptr1(new int(42));
  std::shared_ptr<int> ptr2 = ptr1; // 创建指向相同对象的新指针
  std::cout << *ptr1 << std::endl; // 输出 42
  std::cout << *ptr2 << std::endl; // 输出 42
  return 0;
}

在这个例子中,我们创建了一个std::shared_ptr,并将它复制到另一个std::shared_ptr中。这样,我们就创建了两个指向相同对象的指针,它们的引用计数都为2。当main()函数结束时,这两个指针都将被销毁,并自动释放它们所拥有的内存。在这个例子中,我们创建了一个std::shared_ptr,并将它复制到另一个std::shared_ptr中。这样,我们就创建了两个指向相同对象的指针,它们的引用计数都为2。当main()函数结束时,这两个指针都将被销毁,并自动释放它们所拥有的内存。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值