欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

c++知识复习2.0

程序员文章站 2022-10-31 18:14:11
16: 内存分配方式 内存分配方式有三种: (1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。 (2) 在栈上创建。...

16:
内存分配方式
内存分配方式有三种:
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多

17: return 语句返回常量字符串
char *getstring2(void)
{
char *p = “hello world”;
return p;
}
void test5(void)
{
char *str = null;
str = getstring2();
cout<< str << endl;
}
函数test5 运行虽然不会出错,但是函数getstring2 的设计概念却是错误的。因为getstring2 内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用getstring2,它返回的始终是同一个“只读”的内存块。

18: new/delete, malloc/free
对于非内部数据类型的对象而言,光用maloc/free 无法满足动态对象的要求。对象
在创建的同时要自动执行构造函数, 对象在消亡之前要自动执行析构函数。由于
malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数
和析构函数的任务强加于malloc/free

19: 内存耗尽怎么办
如果在申请动态内存时找不到足够大的内存块,malloc 和new 将返回null 指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
(1)判断指针是否为null,如果是则马上用return 语句终止本函数。例如:
void func(void)
{
a *a = new a;
if(a == null)
{
return;
}

}
(2)判断指针是否为null,如果是则马上用exit(1)终止整个程序的运行。例如:
void func(void)
{
a *a = new a;
if(a == null)
{
cout << “memory exhausted” << endl;
exit(1);
}

}
(3)为new 和malloc 设置异常处理函数。例如visual c++可以用_set_new_hander 函数为new 设置用户自己定义的异常处理函数,也可以让malloc 享用与new 相同的异常处理函数。详细内容请参考c++使用手册。
上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。很多人不忍心用exit(1),问:“不编写出错处理程序,让操作自己解决行不行?”不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果
不用exit(1) 把坏程序杀死,它可能会害死操作系统

20: extern “c”
extern “c”
{
void foo(int x, int y);
? // 其它函数
}
或者写成
extern “c”
{

include “myheader.h”

? // 其它c 头文件
}
这就告诉c++编译译器,函数foo 是个c 连接,应该到库中找名字_foo 而不是找_foo_int_int。c++编译器开发商已经对c 标准库的头文件作了extern“c”处理,所以我们可以用#include 直接引用这些头文件

21: 令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是c++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

22: 运算符重载
运算符 规则
所有的一元运算符 建议重载为成员函数
= () [] -> 只能重载为成员函数
+= -= /= *= &= |= ~= %= >>= <<= 建议重载为成员函数
所有其它运算符 建议重载为全局函数

23: 函数内联
inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
定义在类声明之中的成员函数将自动地成为内联函数,例如
class a
{
public:
void foo(int x, int y) { ? } // 自动地成为内联函数
}
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的
风格,上例应该改成:
// 头文件
class a
{
public:
void foo(int x, int y);
}
// 定义文件
inline void a::foo(int x, int y)
{
?
}

24:构造函数的初始化列表 b::b(const a &a) //初始化表里调用了类a 的拷贝构造函数,从而将成员对象m_a 初始化。 m_a(a)
{

}
b::b(const a &a) //构造函数干了两件事:先暗地里创建m_a对象(调用了a 的无参数构造函数),再调用类a 的赋值函数,将参数a 赋给m_a
{
m_a = a;

}

25: 构造和析构的次序
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序

26: 拷贝构造函数与赋值函数
编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?
string a(“hello”);
string b(“world”);
string c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成string c(a) 以区别于第四个语句

27:
// string 的普通构造函数
string::string(const char *str)
{
if(str==null)
{
m_data = new char[1];
*m_data = ‘/0’;
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}

// 拷贝构造函数
string::string(const string &other)
{
// 允许操作other 的私有成员m_data
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}

// 赋值函数
string & string::operate =(const string &other)
{
// (1) 检查自赋值
if(this == &other)
return *this;
// (2) 释放原有的内存资源
delete [] m_data;
// (3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
// (4)返回本对象的引用
return *this;
}

// string 的析构函数
string::~string(void)
{
delete [] m_data;
// 由于m_data 是内部数据类型,也可以写成 delete m_data;
}

28: 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值
class base
{
public:

base & operate =(const base &other); // 类base 的赋值函数
private:
int m_i, m_j, m_k;
};
class derived : public base
{
public:

derived & operate =(const derived &other); // 类derived 的赋值函数
private:
int m_x, m_y, m_z;
};
derived & derived::operate =(const derived &other)
{
//(1)检查自赋值
if(this == &other)
return *this;
//(2)对基类的数据成员重新赋值
base::operate =(other); // 因为不能直接操作私有数据成员
//(3)对派生类的数据成员赋值
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
//(4)返回本对象的引用
return *this;
}

29;
函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。
例如
class a
{?
a & operate = (const a &other); // 赋值函数
};
a a, b, c; // a, b, c 为a 的对象
?
a = b = c; // 正常的链式赋值
(a = b) = c; // 不正常的链式赋值,但合法
如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。

30: 继承和组合的规则
(1)若在逻辑上b 是a 的“一种”,并且a 的所有功能和属性对b 而言都有意义,则允许b 继承a 的功能和属性。
(2)若在逻辑上a 是b 的“一部分”(a part of),则不允许b 从a 派生,而是要用a 和其它东西组合出b。

31: 引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等