C++ 整理
Posted 懒懒初阳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 整理相关的知识,希望对你有一定的参考价值。
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
IntVec;
这条语句将std::vector
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;
智能指针的另一个常见使用场景是避免空指针的问题。例如,程序需要调用一个函数并返回一个指针,但是有可能函数返回的是空指针,这时可以使用智能指针来判断指针是否为空:
c
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;
- 函数指针
函数指针的一个常见使用场景是回调函数。例如,程序需要实现一个排序算法,可以通过函数指针来指定排序的方式:
arduino
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,2, 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;
```csharp
上面的代码定义了两个比较函数ascending和descending,然后将它们作为函数指针传递给bubble_sort函数,从而实现升序和降序两种排序方式。
c++中并发编程具体如何实现
在C++中,可以通过多线程来实现并发编程。C++标准库提供了一个线程库<thread>
,其中包含了创建和管理线程的类和函数。
通过创建多个线程,可以在不同的线程中执行不同的任务,从而实现并发编程。可以使用std::thread
类创建一个新线程,将需要在新线程中执行的函数作为参数传递给std::thread
类的构造函数。
例如,下面的代码展示了如何使用std::thread
类来创建一个新线程并在其中执行一个函数:
cpp
#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
创建多个线程并执行不同的函数
cpp
#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
保证多线程操作的原子性
cpp
#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
实现线程同步和互斥操作
cpp
#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_guardstd::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),只声明不定义成员函数。
- 析构函数:执行对象销毁时的清理工作,例如释放内存、关闭文件等。
举下具体例子
下面是一个简单的例子,展示了函数、虚函数、纯虚函数和析构函数的使用场景:
c++
#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++ 整理的主要内容,如果未能解决你的问题,请参考以下文章