c++中的指针相关
new
new用来实现运行时分配内存(不同于int a = 123
会在编译时就分配有名称的内存),new会返回指针类型。
使用new有什么用?1、灵活,由于程序不会在编译时进行内存分配,节省内存。
缺点:1、 增加心智负担,要谨防内存泄漏,注意回收内存(只能指针有利于环节内存泄露)。2、new和delete可能导致内存空间不连续,造成内存碎片
内存耗尽
如何没有足够内存满足new请求,就会报异常
delete
用于使用完后释放内存, delete
后面跟指向内存的地址
delete注意事项
1、 不要使用delete释放不是new分配的内存
2、不要释放同一个内存两次
c++中的存储分类
c++有三种管理数据内存方式如下
自动存储
函数内的变量为自动存储,函数执行完毕内部定义的变量会自动销毁。
静态存储
静态存储是整个程序执行期间都存在的存储方式。使用关键字static
动态存储
使用new的变量都是动态存储。这种变量的生命周期受程序员控制。
引用变量
引用变量是为了实现按引用传递参数,超越C语言。
引用变量优点类似于指针,我们修改引用变量原数据也会被改变,但是和指针有一些区别
int refer = 2;
int& refer_ref = refer;
cout << "refer:" << refer << refer_ref << endl; // 2 2
refer_ref = 3;
cout << "refer:" << refer << endl; // 3
引用更像const 指针,必须在创建初始化,且和某个变量关联后无法修改。
int num = 3;
int& num_ref = num; // 等同于 int* const num_ref = #
区别如下
1、语法不同一个是&一个是*
2、声明引用变量时必须初始化,而指针可以先声明再赋值。
智能指针
在 C++ 中,内存管理是一个重要且容易出错的部分。当通过new
操作符动态分配内存后,必须使用delete
操作符来释放内存,否则会导致内存泄漏。智能指针是一种类模板,用于自动管理动态分配的内存,帮助程序员避免忘记释放内存或者错误释放内存(如多次释放同一块内存)等问题。
std::unique_ptr
独占所有权语义:std::unique_ptr
是一种独占式智能指针,它确保同一时刻只有一个unique_ptr
对象拥有一块动态分配的内存。当这个unique_ptr
对象被销毁时(例如离开作用域),它所管理的内存会自动被释放。
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
std::unique_ptr<MyClass> ptr(new MyClass);
// 不需要手动释放内存,ptr离开作用域时会自动调用MyClass的析构函数释放内存
return 0;
}
转移所有权:std::unique_ptr
的所有权可以通过std::move
函数进行转移。例如:
std::unique_ptr<MyClass> ptr1(new MyClass);
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
// 此时ptr1为空,ptr2拥有原来ptr1指向的对象
std::shared_ptr
std::shared_ptr
实现了共享式所有权。多个shared_ptr
对象可以同时拥有同一块动态分配的内存,并且通过引用计数来记录有多少个shared_ptr
对象在引用这块内存。当引用计数为 0 时,即没有shared_ptr
对象引用这块内存时,才会释放内存。
std::shared_ptr<MyClass> ptr1(new MyClass);
std::shared_ptr<MyClass> ptr2 = ptr1;
// 此时ptr1和ptr2都指向同一个MyClass对象,引用计数为2
shared_ptr的问题
std::shared_ptr
可能会出现循环引用的问题。例如,有两个类A
和B
,它们相互包含shared_ptr
成员,当它们的对象通过shared_ptr
相互引用时,会导致引用计数永远不为 0,从而无法释放内存。为了解决这个问题,可以使用std::weak_ptr
。std::weak_ptr
std::weak_ptr
std::weak_ptr
用于解决std::shared_ptr
的循环引用问题。它不增加对象的引用计数,只是作为一个观察者。当需要访问weak_ptr
所指向的对象时,可以通过lock
函数来获取一个shared_ptr
对象,如果对象已经被销毁,lock
函数会返回一个空的shared_ptr
。
class A {
public:
std::shared_ptr<B> ptrB;
};
class B {
public:
std::weak_ptr<A> ptrA;
};
std::shared_ptr<A> a(new A);
std::shared_ptr<B> b(new B);
a->ptrB = b;
b->ptrA = a;
// 在这里,由于B中使用的是weak_ptr,不会导致循环引用,对象最终可以正常销毁
标准库
模板类vector
vector
相当于是动态数组,动态长度。
使用vector必须先引入。
#include<vector>
vector<int> vi;
泛型
在 C++ 中,泛型是一种编程范式,它允许编写能够处理多种数据类型的代码,而不是为每种特定的数据类型编写重复的代码。泛型编程的核心思想是将算法与数据类型分离,使得算法可以应用于不同类型的数据,从而提高代码的复用性和可维护性。
函数模版
函数模板是泛型编程的一种形式,它允许定义一个通用的函数框架,这个函数可以用于多种不同类型的数据。函数模板的定义以template
关键字开头,后面跟着模板参数列表,通常是一个或多个类型参数。例如,下面是一个简单的函数模板,用于交换两个变量的值:
template<typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int a = 10, b = 20;
swap(a, b);
double c = 1.2, d = 3.4;
swap(c, d);
return 0;
}
当调用函数模板时,编译器会根据实际传入的参数类型自动推导出模板参数T
的类型,并生成相应的函数实例。
模版参数的多种形式
除了类型参数(如T
),函数模板还可以有非类型参数,如整数、指针等。例如,下面是一个函数模板,它接受一个整数参数N
,用于打印一个指定大小的数组:
template<typename T, int N>
void printArray(T (&arr)[N]) {
for (int i = 0; i < N; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
类模板
类模板允许创建通用的类定义,其中类的成员类型和成员函数的参数类型可以是模板参数。例如,下面是一个简单的类模板,用于表示一个动态大小的栈:
template<typename T, int MAX_SIZE = 100>
class Stack {
public:
Stack() : top(-1) {}
void push(T value);
T pop();
private:
T data[MAX_SIZE];
int top;
};
成员函数的定义
类模板的成员函数也需要是模板函数,其定义方式有两种。一种是在类模板内部定义成员函数,另一种是在类模板外部定义。在外部定义时,需要在函数定义前加上模板参数列表,以表明这是一个类模板的成员函数。例如,上面Stack
类模板中push
和pop
函数的定义如下(使用类模板时,需要指定模板参数的具体类型和值(如果有非类型参数)):
template<typename T, int MAX_SIZE>
void Stack<T, MAX_SIZE>::push(T value) {
if (top < MAX_SIZE - 1) {
data[++top] = value;
}
}
template<typename T, int MAX_SIZE>
T Stack<T, MAX_SIZE>::pop() {
if (top >= 0) {
return data[top--];
}
throw std::out_of_range("Stack is empty");
}
int main() {
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << intStack.pop() << std::endl;
Stack<double, 200> doubleStack;
doubleStack.push(1.2);
doubleStack.push(3.4);
std::cout << doubleStack.pop() << std::endl;
return 0;
}
右值引用
c++11新增,使用&&
表示。那什么是右值引用呢?右值是指临时对象,或者是不具有持久存储的对象,例如字面量、表达式返回的临时值等。例如:
int&& rvalue_ref = 30; // 30是右值,rvalue_ref是右值引用
那右值引用解决了什么问题呢?主要是避免不必要的复制开销
- 例如,当一个函数返回一个大型对象(如包含大量数据成员的自定义类)时,按照旧的语义,会创建一个临时对象用于返回值,然后通过复制构造函数将这个临时对象复制到接收该返回值的变量中。如果这个对象的数据量很大,或者对象内部包含动态分配的资源(如堆内存),这种复制操作会带来很高的性能成本。
- 右值引用提供了移动语义,使得可以将资源从一个即将销毁的临时对象(右值)转移到另一个对象,而不是进行复制。
移动语义
移动语义就是让编译器知道什么时候需要复制什么时候不需要。
class String {
public:
char* data;
String(const char* str) {
if (str) {
size_t len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
} else {
data = nullptr;
}
}
// 移动构造函数
String(String&& other) noexcept {
data = other.data;
other.data = nullptr;
}
~String() {
delete[] data;
}
};
String getString() {
String local("Hello");
return local;
}
int main() {
String result = getString();
return 0;
}
在getString
函数中返回local
对象时,因为local
是一个局部对象(右值),当String
类有移动构造函数时,编译器会优先使用移动构造函数将local
对象的资源(data
指针所指向的字符数组)移动到result
对象中,而不是进行复制,从而避免了复制local
对象中的字符串数据,从而提高了性能。
完美转发
右值引用还是实现完美转发的关键。在函数模板中,完美转发可以保持参数的原始值类别(左值或右值)不变地传递给其他函数。这在编写泛型代码时非常重要,例如在实现工厂模式或者代理模式等设计模式的泛型版本时。
void processValue(String&& value) {
// 对value进行操作,比如打印它所管理的字符串
std::cout << value.data << std::endl;
}
template<typename T>
void forwardFunction(T&& arg) {
processValue(std::forward<T>(arg));
}
int main() {
String temp("Test");
forwardFunction(temp); // 传递左值
forwardFunction(String("Another Test")); // 传递右值
return 0;
}
在forwardFunction
函数模板中,参数arg
是一个万能引用(根据传入的是左值还是右值可以是左值引用或右值引用)。std::forward<T>(arg)
会根据T
的类型(通过模板参数推导得到),以正确的方式转发arg
。这样就可以确保在调用processValue
函数时,参数的原始值类别得以保持,并且在合适的情况下(如传递右值时)利用移动语义,避免不必要的拷贝。