C++ 整理
C++的数据类型
C++是一种静态类型语言,它支持以下基本数据类型:
- 整型 (int):表示整数,可分为有符号整型(int)和无符号整型(unsigned int),不同类型占用的存储空间大小不同。
- 字符型 (char):表示一个字符,占用一个字节的存储空间。
- 布尔型 (bool):表示真或假,占用一个字节的存储空间。
- 浮点型 (float, double):表示实数,可分为单精度浮点数(float)和双精度浮点数(double),占用的存储空间大小不同。
- 空类型 (void):表示没有值,通常用于函数返回值类型或指针类型。
除了基本数据类型,C++还支持以下扩展数据类型:
- 枚举类型 (enum):表示一组具有名字的整数值,通常用于程序中需要使用固定值的情况。
- 数组类型 (array):表示一组具有相同数据类型的变量集合,可以使用下标操作访问其中的元素。
- 结构体类型 (struct):表示一组不同数据类型的变量集合,可以使用成员访问操作符访问其中的元素。
- 联合类型 (union):表示一组不同数据类型的变量集合,但是只能同时存储其中的一个元素,占用的存储空间大小取决于最大的成员类型大小。
此外,C++还支持指针类型 (pointer),引用类型 (reference)等,这些数据类型在C++的程序设计中具有重要的作用。
c++内存结构与管理
C++是一种直接操作内存的语言,因此需要程序员具备一定的内存管理知识。下面是C++内存结构和内存管理的一些具体内容:
- 内存结构
C++程序的内存可以分为以下几个部分:
- 栈内存(Stack Memory):由编译器自动分配和释放,存储局部变量、函数参数等。栈内存空间大小有限,一般几MB到几十MB不等。
- 堆内存(Heap Memory):由程序员手动申请和释放,存储动态分配的内存空间。堆内存空间大小较大,可以申请GB级别的内存空间。
- 静态内存(Static Memory):存储全局变量和静态变量,程序在运行期间一直存在,直到程序退出才会释放。
- 代码内存(Code Memory):存储可执行代码,由操作系统负责加载到内存中运行。
- 内存管理
C++程序员需要手动管理堆内存的申请和释放,否则会造成内存泄漏或者内存溢出等问题。具体的内存管理包括以下内容:
- 动态内存申请:使用new运算符申请内存空间,并返回指向该空间的指针。例如:int *p = new int;
- 动态内存释放:使用delete运算符释放申请的内存空间,例如:delete p;。需要注意的是,如果使用new[]运算符申请了数组类型的内存空间,需要使用delete[]运算符释放空间,例如:delete[] arr;。
- 内存泄漏:如果程序没有正确释放申请的内存空间,就会造成内存泄漏,即申请的内存空间无法被回收,导致程序的内存使用越来越大。为了避免内存泄漏,需要在程序中明确释放申请的内存空间。
- 空指针异常:如果使用了指针变量,但是没有初始化或者初始化为null指针,就会造成空指针异常,即访问不存在的内存空间,导致程序崩溃。为了避免空指针异常,需要在使用指针变量之前进行初始化,并进行有效性检查。
除了以上内存管理的内容外,C++程序员还需要掌握内存分配器(Memory Allocator)的使用,如STL中的malloc和free函数,以及Boost库中的Memory Pool等高级内存管理技术,以提高程序的内存使用效率和性能。
预处理指令有哪些。使用场景是什么
C++预处理指令是指在编译代码之前由预处理器(preprocessor)处理的特殊指令,它们以#字符开头。预处理器是C++编译器的一部分,用于对代码进行文本替换、宏定义、条件编译等操作,从而在编译时生成最终的源代码。下面是一些常用的预处理指令及其使用场景:
#include
用于包含头文件,将其他文件中的代码复制到当前文件中。头文件中通常包含了函数声明、宏定义和常量等代码,可以提供给当前文件使用。例如:#include
#define
用于宏定义,可以用来定义常量、函数、数据类型等。例如:#define PI 3.14定义了一个名为PI的常量,其值为3.14。
#ifdef,#ifndef,#endif
用于条件编译,可以根据指定的条件编译一部分代码。例如:#ifdef _DEBUG #include
#if,#elif,#else
用于条件编译,可以根据指定的条件编译一部分代码。例如:#if (x > 0) cout << "x is positive" << endl; #elif (x < 0) cout << "x is negative" << endl; #else cout << "x is zero" << endl; #endif表示根据变量x的值输出不同的消息。
#pragma
用于向编译器发送特定的命令或指令,可以用来控制编译器的行为或优化代码。例如:#pragma once表示指示编译器只包含当前文件一次,避免重复包含。
预处理指令可以提高代码的可读性和灵活性,可以根据不同的平台、编译器或者操作系统等定义不同的宏,从而实现跨平台编译或者编译不同版本的代码。同时也可以使用条件编译实现不同的调试或者发布版本,提高代码的可维护性和适应性。
Typedef别名
在C++中,typedef是一种定义类型别名的方法,它可以将一个类型名定义为另一个类型的别名,从而方便程序员使用。以下是常见的typedef别名及其使用场景:
- typedef int INT;
这条语句将int类型定义为INT的别名,使用INT可以代替int,例如:INT a = 10;。
- typedef char* PCHAR;
这条语句将char_类型定义为PCHAR的别名,使用PCHAR可以代替char_,例如:PCHAR str = "Hello World!";。
- typedef struct tagStudent { char name[20]; int age; } STUDENT;
这条语句将struct tagStudent类型定义为STUDENT的别名,使用STUDENT可以代替struct tagStudent,例如:STUDENT stu = { "Tom", 18 };。
- typedef void (*FUNC_PTR)(int);
这条语句将void ()(int)类型定义为FUNC_PTR的别名,使用FUNC_PTR可以代替void ()(int),例如:FUNC_PTR pFunc = func;。
typedef std::vector<int> IntVec
;
这条语句将std::vector<int>
类型定义为IntVec的别名,使用IntVec可以代替std::vector<int>
,例如:IntVec vec; vec.push_back(1);。
typedef别名的使用场景主要有以下几个方面:
-
提高代码的可读性和可维护性,使用别名可以使代码更加易懂,避免使用复杂或难以理解的类型名称。
-
简化代码编写,使用别名可以简化代码编写,提高效率。
-
便于跨平台编译,使用别名可以在不同的编译器或平台上保持一致的类型名称,避免因不同编译器或平台的差异导致的编译错误。
-
便于类型变更和维护,使用别名可以使类型的变更更加方便和安全,减少代码的修改和维护成本。
c++中结构体与共用体
在C++中,结构体(struct)和共用体(union)都是用户自定义的数据类型,可以用于定义复杂的数据结构。
结构体是一种数据类型,它可以包含多个不同的数据类型,用于描述一个对象或实体的多个属性。结构体中的每个属性称为成员变量,可以是基本数据类型或其他结构体。结构体中的成员变量按照定义的顺序依次排列,每个成员变量都可以通过结构体变量名和成员名来访问。
共用体是一种特殊的结构体,它只能同时存储一个成员变量的值。共用体的成员变量共享同一块内存空间,因此只能访问其中一个成员变量,访问时需要指定成员变量的名称。共用体通常用于节省内存或在不同的数据类型之间进行转换。
以下是结构体和共用体的使用场景:
-
结构体通常用于描述具有多个属性的对象或实体,例如:学生、汽车、房屋等。结构体可以将多个属性封装在一起,方便进行数据操作和管理。
-
共用体通常用于需要在不同的数据类型之间进行转换的场景,例如:网络通信、文件读写等。共用体可以将不同类型的数据存储在同一块内存空间中,从而避免了数据类型转换时的数据拷贝和类型转换。
-
结构体和共用体都可以用于定义复杂的数据结构,例如:树、图等。通过定义结构体和共用体,可以将不同的数据类型组合在一起,构成复杂的数据结构,方便数据的操作和管理。
总之,结构体和共用体都是C++中非常重要的用户自定义数据类型,它们的使用场景非常广泛,可以帮助程序员更加方便和灵活地进行数据操作和管理。
指针、智能指针、方法指针 具体使用。及其定义和使用场景
指针、智能指针和函数指针都是C++中非常重要的数据类型,它们各自有着不同的定义和使用场景,下面分别进行介绍:
- 指针
指针是一种特殊的数据类型,它可以存储内存地址。通过指针,程序可以直接访问内存中的数据。指针的定义方式为:类型名 *指针变量名;例如:int *p;定义了一个名为p的整型指针变量。
指针的主要使用场景包括:
- 动态内存分配和释放
- 数组访问
- 传递函数参数
- 智能指针
智能指针是一种特殊的指针,它可以自动管理内存的释放。智能指针通常用于动态分配的内存,可以避免程序员忘记释放内存所带来的问题。C++11中提供了两种智能指针:shared_ptr和unique_ptr。
shared_ptr是一种引用计数的智能指针,它会自动记录指向的对象的引用次数,当引用次数为0时,自动释放内存。shared_ptr的定义方式为:shared_ptr<类型>指针变量名;例如:shared_ptr
unique_ptr是一种独占所有权的智能指针,它可以确保只有一个指针变量指向对象,并且当指针变量离开作用域时,自动释放内存。unique_ptr的定义方式为:unique_ptr<类型>指针变量名;例如:unique_ptr
智能指针的主要使用场景包括:
- 动态内存分配和释放
- 避免内存泄漏
- 函数指针
函数指针是指向函数的指针变量,可以将函数名作为参数传递给函数指针,从而实现函数的动态调用。函数指针的定义方式为:返回类型 (*指针变量名)(参数列表);例如:int (*p)(int, int);定义了一个名为p的指向返回值为int,参数列表为int和int的函数指针变量。
函数指针的主要使用场景包括:
- 回调函数
- 函数动态调用
- 多态实现
总之,指针、智能指针和函数指针都是C++中非常重要的数据类型,它们各自有着不同的定义和使用场景。程序员需要根据具体的需求来选择适合的指针类型,从而实现代码的高效和灵活。
具体使用
下面分别以指针、智能指针和函数指针为例,举例说明它们的具体使用场景和用法:
- 指针
指针的一个常见使用场景是动态内存分配和释放。例如,程序需要在运行时动态创建一个数组来存储数据,可以使用new运算符分配内存,然后使用指针来访问这个数组:
int size = 10;
int *arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i;
}
delete[] arr; // 释放内存
指针的另一个常见使用场景是传递函数参数。例如,程序需要交换两个变量的值,可以使用指针作为函数参数来实现:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 1, b = 2;
swap(&a, &b);
return 0;
}
- 智能指针
智能指针的一个常见使用场景是动态内存分配和释放。例如,程序需要动态创建一个对象并使用,可以使用智能指针来管理内存,从而避免内存泄漏:
class MyClass {
public:
MyClass() { cout << "MyClass constructor" << endl; }
~MyClass() { cout << "MyClass destructor" << endl; }
};
int main() {
shared_ptr<MyClass> p(new MyClass());
return 0;
}
智能指针的另一个常见使用场景是避免空指针的问题。例如,程序需要调用一个函数并返回一个指针,但是有可能函数返回的是空指针,这时可以使用智能指针来判断指针是否为空:
unique_ptr<int> create_int() {
int *p = new int(123);
return unique_ptr<int>(p);
}
int main() {
unique_ptr<int> p = create_int();
if (p) {
cout << *p << endl;
} else {
cout << "null pointer" << endl;
}
return 0;
}
- 函数指针
函数指针的一个常见使用场景是回调函数。例如,程序需要实现一个排序算法,可以通过函数指针来指定排序的方式:
void bubble_sort(int *arr, int n, bool (*cmp)(int, int)) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (cmp(arr[j], arr[j + 1])) {
swap(arr[j], arr[j + 1]);
}
}
}
}
bool ascending(int a, int b) {
return a < b;
}
bool descending(int a, int b) {
return a > b;
}
int main() {
int arr[] = { 3, 1, 4, 1, 5, 9, 6, 5, 3 };
int n = sizeof(arr) / sizeof(int);
bubble\_sort(arr, n, ascending);
for (int i = 0; i < n; i++) {
cout << arr\[i\] << " ";
}
cout << endl;
bubble\_sort(arr, n, descending);
for (int i = 0; i < n; i++) {
cout << arr\[i\] << " ";
}
cout << endl; return 0;
}
上面的代码定义了两个比较函数ascending和descending,然后将它们作为函数指针传递给bubble_sort函数,从而实现升序和降序两种排序方式。
c++中并发编程
在C++中,可以通过多线程来实现并发编程。C++标准库提供了一个线程库<thread>
,其中包含了创建和管理线程的类和函数。
通过创建多个线程,可以在不同的线程中执行不同的任务,从而实现并发编程。可以使用std::thread
类创建一个新线程,将需要在新线程中执行的函数作为参数传递给std::thread
类的构造函数。
例如,下面的代码展示了如何使用std::thread
类来创建一个新线程并在其中执行一个函数:
#include <iostream>
#include <thread>
void myFunction() {
std::cout << "This is myFunction" << std::endl;
}
int main() {
std::thread myThread(myFunction);
myThread.join();
return 0;
}
在上面的代码中,myFunction
函数会在新线程中执行。myThread.join()
会等待新线程执行完毕,然后程序才会继续往下执行。
另外,C++11还提供了一个原子类型std::atomic
,它可以保证在多线程并发操作时的原子性,从而避免了多线程竞争导致的数据不一致问题。C++11还提供了一个互斥量std::mutex
和一个条件变量std::condition_variable
,可以用来实现线程同步和互斥操作。
除了C++标准库提供的多线程机制之外,还可以使用第三方库,比如Boost和Poco等,它们提供了更丰富的多线程功能,例如线程池、定时器、消息队列等,可以更方便地进行并发编程。
下面是一些具体实现实例:
- 使用
std::thread
创建多个线程并执行不同的函数
#include <iostream>
#include <thread>
#include <chrono>
void function1() {
std::cout << "Thread 1 is running" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "Thread 1 finished" << std::endl;
}
void function2() {
std::cout << "Thread 2 is running" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
std::cout << "Thread 2 finished" << std::endl;
}
int main() {
std::thread t1(function1);
std::thread t2(function2);
std::cout << "Main thread is running" << std::endl;
t1.join();
t2.join();
std::cout << "All threads finished" << std::endl;
return 0;
}
在上面的代码中,我们创建了两个线程,分别执行function1
和function2
函数。std::this_thread::sleep_for
函数可以让线程休眠指定的时间。t1.join()
和t2.join()
语句会等待线程t1
和t2
执行完毕后,程序才会继续往下执行。
- 使用
std::atomic
保证多线程操作的原子性
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 10000; ++i) {
++counter;
}
}
void decrement() {
for (int i = 0; i < 10000; ++i) {
--counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(decrement);
t1.join();
t2.join();
std::cout << "Counter = " << counter << std::endl;
return 0;
}
在上面的代码中,我们创建了两个线程t1
和t2
,分别执行increment
和decrement
函数。counter
是一个std::atomic<int>
类型的变量,可以保证多线程操作时的原子性,从而避免了多线程竞争导致的数据不一致问题。
- 使用
std::mutex
和std::condition_variable
实现线程同步和互斥操作
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
void worker_thread() {
std::unique_lock<std::mutex> lock(mutex);
while (!ready) {
cv.wait(lock);
}
std::cout << "Worker thread is running" << std::endl;
}
int main() {
std::thread worker(worker_thread);
std::cout << "Main thread is running" << std::endl;
{
std::lock_guard<std::mutex> lock(mutex);
ready = true;
}
cv.notify_one();
worker.join();
std::cout << "All threads finished" << std::endl;
return 0;
}
std::lock_guard<std::mutex> lock(mutex); 这句的作用是什么
std::lock_guard<std::mutex>
是一个RAII风格的互斥锁保护类,用于保护一个临界区,以确保同一时间只有一个线程可以访问该临界区。
lock_guard
在构造函数中获取互斥锁,在析构函数中释放互斥锁。因此,使用std::lock_guard<std::mutex>
定义的对象,在作用域结束时,会自动释放互斥锁,避免了手动释放互斥锁的麻烦。
在上面的例子中,我们使用std::lock_guard<std::mutex>
来保护ready
变量的读写操作,防止多线程访问时出现数据竞争的问题。当lock_guard
对象的生命周期结束时,互斥锁会自动释放,从而避免了程序员手动释放锁的操作。
函数、虛函数、纯虛函数与析构函数
函数是指一组执行特定任务的代码,可以接受参数并返回值。在C++中,函数可以用于封装重复使用的代码块,提高代码的可读性和可维护性。
虚函数是在基类中声明的,用virtual
关键字修饰的成员函数。虚函数可在派生类中重写(override)或重载(overload),实现运行时多态性。使用虚函数可以让基类指针或引用调用派生类的成员函数,实现动态绑定。
纯虚函数是在基类中声明的,没有函数体的虚函数,即在函数声明语句后面加上=0
。派生类必须重写所有纯虚函数,否则该派生类也是抽象类。使用纯虚函数可以实现接口(interface),即只声明不定义成员函数。
析构函数是类的特殊成员函数,用于在对象销毁时执行清理工作,例如释放内存、关闭文件等。析构函数的命名规则为~类名()
,不接受参数,不返回值。析构函数在对象销毁时自动调用,不需要显式调用。
运用场景:
- 函数:封装代码块,提高代码的可读性和可维护性。
- 虚函数:实现多态性,让基类指针或引用调用派生类的成员函数。
- 纯虚函数:实现接口(interface),只声明不定义成员函数。
- 析构函数:执行对象销毁时的清理工作,例如释放内存、关闭文件等。
下面是一个简单的例子,展示了函数、虚函数、纯虚函数和析构函数的使用场景:
#include <iostream>
#include <string>
using namespace std;
// 基类
class Animal {
public:
// 普通成员函数
void eat(const string& food) {
cout << "Animal eats " << food << endl;
}
// 虚函数
virtual void move() {
cout << "Animal moves" << endl;
}
// 纯虚函数
virtual void sound() = 0;
// 析构函数
virtual ~Animal() {
cout << "Animal is destroyed" << endl;
}
};
// 派生类1
class Dog : public Animal {
public:
// 重写虚函数
void move() override {
cout << "Dog runs" << endl;
}
// 实现纯虚函数
void sound() override {
cout << "Dog barks" << endl;
}
// 析构函数
~Dog() {
cout << "Dog is destroyed" << endl;
}
};
// 派生类2
class Cat : public Animal {
public:
// 重写虚函数
void move() override {
cout << "Cat climbs" << endl;
}
// 实现纯虚函数
void sound() override {
cout << "Cat meows" << endl;
}
// 析构函数
~Cat() {
cout << "Cat is destroyed" << endl;
}
};
int main() {
Animal* ptr_animal = new Dog(); // 派生类对象可以赋给基类指针
ptr_animal->eat("meat"); // 调用基类函数
ptr_animal->move(); // 调用派生类函数
ptr_animal->sound(); // 调用派生类函数
delete ptr_animal; // 调用派生类析构函数
ptr_animal = new Cat(); // 派生类对象可以赋给基类指针
ptr_animal->eat("fish"); // 调用基类函数
ptr_animal->move(); // 调用派生类函数
ptr_animal->sound(); // 调用派生类函数
delete ptr_animal; // 调用派生类析构函数
return 0;
}
在这个例子中,Animal
是一个基类,Dog
和Cat
是两个派生类,都继承了Animal
。Animal
中有一个普通成员函数eat()
,一个虚函数move()
,一个纯虚函数sound()
,以及一个析构函数~Animal()
。Dog
和Cat
都重写了move()
和sound()
,并实现了各自的析构函数。在main()
函数中,我们创建了一个Dog
对象和一个Cat
对象,并通过基类指针调用它们的成员函数,包括eat()
、move()
和sound()
。注意,派生类对象可以赋给基类指针,这样就可以通过基类指针调用
c++ 初始化列表
C++的初始化列表是用于初始化成员变量的一种方式,可以在构造函数中通过初始化列表来对类的成员变量进行初始化。
下面是一个简单的例子:
c++
class Person {
public:
Person(int age, std::string name) : age_(age), name_(name) {}
private:
int age_;
std::string name_;
};
在这个例子中,构造函数使用初始化列表来对age_
和name_
成员变量进行初始化。注意,成员变量的初始化顺序与它们在类中的声明顺序一致,而不是在初始化列表中的顺序。
初始化列表的使用场景包括:
- 初始化成员变量,尤其是const和引用类型的成员变量,因为它们在构造函数中无法被赋值。
- 在构造函数中调用基类的构造函数。
- 初始化非静态成员对象,如std::mutex,这些对象的构造函数可能会抛出异常,所以最好在初始化列表中初始化它们。
需要注意的是,如果成员变量没有被初始化列表初始化,它们将使用默认构造函数进行初始化,如果没有默认构造函数,会导致编译错误。同时,初始化列表不能对static成员变量进行初始化。
C++指针类型有哪些
C++中指针类型有以下几种:
-
普通指针:指向数据类型的指针,使用
*
解引用操作符来访问指向的值。c++
int value = 42; int *p = &value; *p = 24; std::cout << value << std::endl; // 输出24
-
常量指针:指向常量数据类型的指针,无法通过指针修改指向的值。
c++
const int value = 42; const int *p = &value; // *p = 24; // 编译错误
-
指向常量的指针:指向可变数据类型的常量指针,无法通过指针修改指向的值。
c++
int value = 42; int *const p = &value; *p = 24; // p = nullptr; // 编译错误
-
常量指针常量:指向常量数据类型的常量指针,无法通过指针修改指向的值和指针本身的值。
c++
const int value = 42; const int *const p = &value; // *p = 24; // 编译错误 // p = nullptr; // 编译错误
指针类型的使用场景包括:
- 传递函数参数时,可以使用指针来避免拷贝大型数据结构,提高效率。
- 动态内存分配时,使用指针来管理内存,可以灵活地分配和释放内存空间。
- 在函数中返回多个值时,可以通过指针来返回多个值。
- 在操作数组和字符串时,使用指针可以遍历和修改数组和字符串的元素。
- 在实现数据结构和算法时,使用指针可以方便地访问和修改数据结构的内部元素。
c++ 中利用 mutex、condition_variable、 lock_guard
在C++中,mutex
、condition_variable
和lock_guard
是用于多线程编程的重要工具,主要用于实现线程同步和互斥。下面分别介绍一下它们的具体用法:
mutex
:互斥锁,用于保护共享资源的访问,防止多个线程同时访问同一个资源而导致数据出现错误。使用std::mutex
定义互斥锁,然后使用lock()
和unlock()
方法进行加锁和解锁。示例代码如下:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 定义互斥锁
void print(int i)
{
mtx.lock(); // 加锁
std::cout << "Thread " << i << " is running" << std::endl;
mtx.unlock(); // 解锁
}
int main()
{
std::thread t1(print, 1);
std::thread t2(print, 2);
t1.join();
t2.join();
return 0;
}
condition_variable
:条件变量,用于线程之间的通信和同步。可以通过wait()
方法等待另一个线程发出通知,也可以通过notify_one()
或notify_all()
方法通知其他线程继续执行。使用std::condition_variable
定义条件变量。示例代码如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Worker is running" << std::endl;
}
void master()
{
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
}
int main()
{
std::thread t1(worker);
std::thread t2(master);
t1.join();
t2.join();
return 0;
}
lock_guard
:锁保护器,用于在作用域内自动锁定互斥锁,并在作用域结束时自动解锁。使用std::lock_guard
定义锁保护器。示例代码如下:
c++
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print(int i)
{
std::lock_guard<std::mutex> lock(mtx); // 加锁
std::cout << "Thread " << i << " is running" << std::endl;
} // 解锁
int main()
{
std::thread t1(print, 1);
std::thread t2(print, 2);
t1.join();
t2.join();
return 0;
}
std::lock_guardstd::mutex lock(mtx); 可以替代 mtx.lock(); // 加锁 mtx.unlock();
可以使用 std::lock_guard<std::mutex>
来替代 mtx.lock();
和 mtx.unlock();
操作,其会在构造函数中自动加锁,析构函数中自动解锁,确保在出现异常等情况时也能正确释放锁资源,更加安全和方便。例如:
std::mutex mtx;
// 使用 std::lock_guard 来加锁和解锁互斥量 mtx
void foo() {
std::lock_guard<std::mutex> lock(mtx);
// 临界区代码
}
在 foo
函数的执行期间,互斥量 mtx
将会被自动加锁,函数执行完后会自动解锁。这种方式比较简单,同时也避免了忘记解锁的问题。
指针常量 和 常量指针的区别
指针常量和常量指针都是指针类型的修饰符,它们的区别在于指针所指向的内容(也就是指针指向的内存地址中的值)是否可以被修改。
指针常量是指针本身是一个常量,也就是指针本身的值不能被修改,但是可以通过指针修改指向的内容。例如:
c++
int a = 10;
int b = 20;
int *const p = &a; // 定义一个指针常量 p,指向变量 a,p 的值不能被修改
*p = 30; // 可以修改指针 p 所指向的变量 a 的值
// p = &b; // 编译错误,无法修改指针常量 p 的值
常量指针是指指针所指向的内容是一个常量,也就是指针所指向的变量的值不能被修改,但是指针本身的值可以被修改。例如:
int a = 10;
int b = 20;
const int *p = &a; // 定义一个常量指针 p,指向变量 a,不能通过 p 修改 a 的值
// *p = 30; // 编译错误,无法通过常量指针 p 修改 a 的值
p = &b; // 可以修改指针 p 所指向的地址,指向变量 b
需要注意的是,指针常量和常量指针都可以用 const
关键字进行修饰,从而增加代码的可读性和安全性。例如,指向常量的指针可以这样定义:
const int *p = &a; // 指向常量的指针,不能通过 p 修改 a 的值
常量指针也可以这样定义:
int a = 10;
int b = 20;
int *const p = &a; // 指针常量,p 的值不能被修改
const int *const q = &a; // 常量指针常量,既不能通过 q 修改 a 的值,也不能修改 q 的值
c++中指针拷贝和对象拷贝
在C++中,指针拷贝和对象拷贝有以下不同:
- 指针拷贝:将一个指针变量的值(即指针所指的地址)赋给另一个指针变量,此时两个指针变量指向的是同一个地址,它们共享同一块内存空间。例如:
int a = 10;
int *p1 = &a;
int *p2 = p1; // 指针拷贝,p2和p1指向同一个地址
- 对象拷贝:将一个对象的值复制给另一个对象,此时两个对象独立存在,它们分别占据不同的内存空间。对象拷贝可以通过拷贝构造函数或者赋值运算符来实现。例如:
class Person {
public:
int age;
string name;
// 拷贝构造函数
Person(const Person &p) {
age = p.age;
name = p.name;
}
// 赋值运算符
Person &operator=(const Person &p) {
if (this != &p) {
age = p.age;
name = p.name;
}
return *this;
}
};
Person p1;
p1.age = 20;
p1.name = "Tom";
Person p2 = p1; // 对象拷贝,p2和p1是两个独立的对象
需要注意的是,对象拷贝可能存在浅拷贝和深拷贝之分,如果对象包含指针类型的成员变量,浅拷贝会导致两个对象共享同一块内存空间,而深拷贝则会为每个对象分配独立的内存空间,避免共享带来的问题。
c++中 STL
STL(Standard Template Library)是 C++ 的一个标准库,提供了一组通用的模板类和函数,包括容器、算法和迭代器等。STL 的设计思想是基于泛型编程,即提供一些通用的模板类和函数,能够适用于各种数据类型。
STL 提供了许多容器类,包括 vector、list、map、set 等,这些容器类可以存储不同类型的数据,并提供了各种操作这些数据的方法。STL 还提供了许多算法,如排序、查找、合并等,这些算法可以用于不同类型的容器,提高了代码的重用性和可读性。STL 中还有迭代器,它提供了一种访问容器中元素的通用方式,可以在容器内部遍历元素。
使用 STL 可以提高 C++ 代码的效率、可读性和可维护性,同时还能够减少开发时间和代码量。由于 STL 是标准库的一部分,因此可以保证在不同的平台上都可以使用,并且具有较好的可移植性。
常见使用STL的例子
以下是一些常见的使用 STL 的例子:
- 使用 vector 存储一组数据,并通过迭代器遍历:
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
for (auto it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
- 使用 set 存储一组唯一的数据:
#include <iostream>
#include <set>
int main() {
std::set<int> nums = {1, 2, 3, 4, 5, 5, 4, 3, 2, 1};
for (auto it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
- 使用 map 存储一组键值对,并通过迭代器遍历:
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> nums = {{"one", 1}, {"two", 2}, {"three", 3}};
for (auto it = nums.begin(); it != nums.end(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
return 0;
}
- 使用算法库中的 sort 函数对 vector 进行排序:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {5, 4, 3, 2, 1};
std::sort(nums.begin(), nums.end());
for (auto it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
c++中文件io
在 C++ 中,文件 I/O 是通过流 (stream) 来完成的。常见的文件 I/O 处理方式包括:
-
打开文件:使用标准库中的
std::ifstream
和std::ofstream
类来打开文件,分别用于读取和写入文件。文件打开后,可以进行读取和写入操作。 -
写入数据:使用流插入运算符
<<
将数据写入文件中,如fout << "Hello World!"
。 -
读取数据:使用流提取运算符
>>
从文件中读取数据,如fin >> num
。 -
关闭文件:使用
close()
方法关闭文件,释放相关资源。
例如,下面的示例展示了如何使用 C++ 中的文件 I/O 处理方式读取文件中的整数并计算它们的和:
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ifstream fin("data.txt"); // 打开文件
int num, sum = 0;
while (fin >> num) { // 读取文件中的整数并计算它们的和
sum += num;
}
fin.close(); // 关闭文件
std::cout << "The sum of integers in data.txt is: " << sum << std::endl;
return 0;
}
上述代码中,使用 std::ifstream
类打开文件 data.txt
,并使用 >>
运算符从文件中逐个读取整数并计算它们的和。最后使用 close()
方法关闭文件。
需要注意的是,在进行文件 I/O 操作时,需要确保文件的存在和正确性,并且需要避免文件打开失败、读写错误等异常情况的发生。