C++对内存的控制和管理严格到令人发指的地步,所以关于动态内存,关于new/delete运算符,一定要十分熟悉!C++中的new/delete和C中malloc/free区别也要明白。
C++的动态内存分配
1 回顾C语言中动态内存分配
1)分配:malloc()/calloc()/realloc()
2)释放:free()
3)错误处理:返回值
2 C++使用new/delete运算符分配内存
1)分配:new/new[]
2)释放:delete/delete[]
3)错误处理:异常(后面讲)
*注:带 [ ] 表示对数组类型的内存进行操作
/*
注释为C语言的动态分配内容方式
*/
//int* p = (int*)malloc(sizeof(int));
int* p = new int;
*p = 100;
//free(p);
delete p;
---------------------------------------
//int* p = (int*)malloc(sizeof(int));
//*p = 0;
int* p = new int(0);//分配内存同时初始化
*p = 100;
//free(p)
delete p;
=======================================
//int* pa = (int*)malloc(sizeof(int)*10);
int* pa = new int[10];
pa[2] = 123;
cout << *(p+2) << endl; //输出123,此时+2,加的是两个int
//free(pa);
delete[] pa;
**拓展:new的用法
根据new的定义,new有三种用法,
1 void* operator new (std::size_t size) throw (std::bad_alloc);
这种用法申请内存,失败时抛出bac_alloc异常
如 int p = new int;
Class A;
A* p = new A;//申请内存并初始化ClassA, 调用A的构造函数
2 void* operator new (std::size_t size, const std::nothrow_t& nothrow_constant) throw();
这种用法申请内存,失败时返回空NULL
A* a = new(std::nothrow) A;
3 void* operator new (std::size_t size, void* ptr) throw();
这种用法是placement new,用于在给定的内存空间中初始化变量或对象
char buf[100];
A* p = new(buf)A;//在buf上初始化Class A,调用A的构造函数
C++的引用(Reference)
1 定义
1)引用就是某个变量别名,对引用的操作与对该变量的操作相同。
2)语法:
类型& 引用名(别名) = 变量;
注:引用在定义时要绑定一个变量,初始化以后不能修改引用的目标。
注:引用的类型与绑定变量类型要一致。
eg:
int a = 100;
int& b = a;//b就是a的别名
b++;//等价于a++;
cout << a << endl;//101
int c = 200;
b = c;//将c的值赋值给b,等价于给a赋值
cout << a << endl;//200
2 常引用
1)定义引用时加const修饰,即为常引用,不能通过常引用修改引用的目标。
2)语法:
const 类型& 引用名(别名) = 变量/右值;
eg:
int a = 0;
const int& b = a;//b就是a的常引用。
b = 10;//error
注:普通的引用只能引用左值,而常引用既可以引用左值也可以引用右值,所以也可以把常引用称为万能引用。
eg:
int& r = 100;//error
const int& r = 100;//ok
**注:关于左值和右值
1)左值:可以放在赋值运算符左侧,存取数据
-->普通的变量都是左值
-->前++/--表达式结果是左值
int num = 0;
++num = 10;//ok
-->赋值表达式结果是左值
int a = 0;
int b = 10;
(a = b) = 20;
cout << a << ',' << b << endl;//20,10
2)右值:只能放在赋值运算符右侧
-->字面值常量都是右值
-->大多数表达式结果都是右值
eg:后++/-- 算符运算 逻辑运算 ...
int num = 0;
num++ = 10;//error
int a = 3,b =5;
(a + b) = 20;//error
3 引用型函数参数
1)将引用用于函数的参数,可以直接修改实参变量的值,同时减小函数调用的开销。
2)引用参数有可能意外修改实参变量的值,如果不希望修改实参本身,可以将形参定义为常引用,提高传参效率的同时还可以接收常量型的实参。
#include <iostream>
using namespace std;
struct Student{
char name[128];
int age;
};
void print(const Student& s){
cout << s.name << ',' << s.age/*++*/ << endl;
}
int main(void)
{
const Student student = {"张飞",25};
print(student);
print(student);
return 0;
}
4 引用型函数返回值
1)可以将函数的返回值声明为引用,避免函数返回值所带来的内存开销。
2)如果一个返回类型被声明为引用,那么该函数返回值可能是一个左值。
3)为了避免在函数外部修改引用的目标变量,可以返回常引用。
注:不要从函数中返回局部变量的引用,因为所引用的目标变量内存会在函数调用结束以后被释放,可以返回成员变量、静态变量、全局变量的引用。
#include <iostream>
using namespace std;
struct A{
int data;
/*const*/ int& foo(void){
return data;
}
//返回局部变量引用,危险!
int& bar(void){
int a = 100;
return a;
}
};
int main(void)
{
A a = {0};
cout << a.data << endl;//0
//a.data = 100
a.foo() = 100;
cout << a.data << endl;//100
return 0;
}
5 引用和指针:
可以看到,引用就像指针一样,减少内存开销,提升程序效率,而且可读性更好,那指针和引用有什么区别呢?
1)引用的本质就是指针
int i = 100;
int& ri = i;
int* const pi = &i;
ri《==》*pi
2)指针可以不做初始化,其指向的目标也可以在初始化以后再修改(指针常量例外);而引用必须初始化,一旦初始化其引用目标不能再做修改。
eg:
int a=3,b=5;
int* p;//ok
p = &a;//p指向a
p = &b;//p指向b
-------------------
int& r;//error
int& r = a;
r = b;//不是修改引用目标而是将b值赋值给a
====================
3)可以定义指针的指针(二级指针),但是不能定义引用的指针。
eg:
int a = 100;
int* p = &a;
int** pp = &p;//ok,二级指针
-----------
int& r = a;
int&* pr = &r;//error
4)可以定义指针的引用(指针的别名),但不能定义引用的引用。
eg:
int a = 100;
int* p = a;
int*& rp = p;//ok
---------------
int& r = a;
int&& rr = r;//error,//在C++11中称为右值引用
5)可以定义指针数组,但是不能定义引用数组
int a=1,b=2,c=3;
int* parr[3] = {&a,&b,&c}; //ok
int& rarr[3] = {a,b,c};//error
6)可以定义数组的引用(数组的别名)
int arr[3] = {1,2,3};
int (&rarr)[3] = arr;//ok
7)和函数指针一样,也可以定义函数的引用,其语法特性和函数指针一致。
eg:
void func(int a,double d){}
int main(void){
//函数指针
void (*pfunc)(int,double) = func;
pfunc(10,3.14);
//函数的引用(函数的别名)
void (&rfunc)(int,double) = func;
rfunc(10,3.14);
}
类型转换
1 隐式类型转换
eg:
char c = 'A';
int i = c;//隐式
void foo(int i){}
foo(c);//隐式
int fun(void){
char c = 'A';
return c;//隐式
}
2 C++兼容C语言显式类型转换(强制类型转换)
eg:
int* p = (int*)malloc(4); //强制类型转换
eg:
char c = 'A';
int i = (int)c;//强制类型转换
int i = int(c);//C++风格的强制转换
3 C++增加了四种操作符形式显式类型转换
1)静态类型转换
语法:
目标类型变量=static_cast<目标类型>(源类型变量);
适用场景:主要用于将void*转换为其它的指针
eg:
int a = 100;
void* pv = &a;
//int* pi = pv;//不能隐式转换,只能显式转换
int* pi = static_cast<int*>(pv);
2)动态类型转换(初学者可先跳过这条)
语法:
目标类型变量=dynamic_cast<目标类型>(源类型变量);
适用场景:将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数。前提是该基类至少含有一个虚函数(比如虚析构函数)。
class Base{
Base();
virtual ~Base();//至少有个一个虚函数
};
class Child : public Base{
Child();
~Child();
};
int main(){
Base* pb = new Child;
Chile* pc = dynamic_cast<Chile*>(pb);
return 0;
}
3)常类型转换
语法:
目标类型变量=const_cast<目标类型>(源类型变量);
适用场景:
主要用于去除一个指针或引用的常属性。
eg:
const int a = 10;
const int* pa = &a;
*pa = 20;//error
int* p2 = const_cast<int*>(pa);
*p2 = 20;//ok
4)重解释类型转换
语法:
目标类型变量 = reinterpret_cast<目标类型>(源类型变量);
适用场景:
--》在指针和整形数之间的转换
--》任意类型的指针或引用之间转换
eg:物理内存0x12345678存放一个数据100;
int* p = reinterpret_cast(int*)(0x12345678);
*p = 100;
C++之父本贾尼·斯特劳斯特卢普给C程序建议
1)慎用宏,用const、enum和inline替换
#define PAI 3.14
==》const double PAI = 3.14;
#define STATE_SLEEP 0
#define STATE_RUN 1
#define STATE_STOP 2
==》enum STATE{SLEEP,RUN,STOP};
#define max(a,b) ((a)>(b)?(a):(b))
==》inline int max(int a,int b){
return a > b ? a : b;
}
2)变量随用随声明同时初始化
3)尽量适用new和delete少用malloc和free
4)少用void*、指针计算
5)少用联合体、强制转换
6)尽量同string类型表示字符串,少用C风格char*