这几天用了晚上些空余时间,将林锐的高质量C++/C编程这本书看了一遍,为了加深印象,总结下一些之前不太清楚的知识点,和一些比较重要的知识点。
(1)文件结构:
防止头文件重复引用:
方式一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
... ... // 声明、定义语句
#endif
方式二:
#pragma once
... ... // 声明、定义语句
两种存在细微的区别:
#ifndef方式受C++/C语言标准支持,其原理是根据头文件的宏判定是否有重复定义,所以编译器每次都要打开头文件,编译时间稍长
#pragma once是根据头文件在物理上的一个文件是否相同来判定重复定义的,编译时间是短了,但是可能一个头文件有多份copy,就不能排除了,而且#pragma once 有些老的编译器是不支持的,兼容性不好
(2)类中常量:
不能在类声明中初始化 const 数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道 SIZE的值是什么。只有在类的对象创建时,才会对初始化类中的数据成员
class A
{…
const int SIZE = 100; // 错误,企图在类声明中初始化 const 数据成员
int array[SIZE]; // 错误,未知的 SIZE
};
const 数据成员的初始化只能在类构造函数的初始化表中进行,例如
class A
{…
A(int size); // 构造函数
const int SIZE ;
};
A::A(int size) : SIZE(size) // 构造函数的初始化表
{
…
}
A a(100); // 对象 a 的 SIZE 值为 100
A b(200); // 对象 b 的 SIZE 值为 200
怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中
的枚举常量来实现。例如
class A
{…
enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:
它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)
(3)函数设计是参数传递时应遵守的规则:
【1】:如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改
【2】:如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率
【3】:避免函数有太多的参数,参数个数尽量控制在 5 个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
(4)值传递和引用传递:
class String
{…
// 赋值函数
String & operate=(const String &other);
// 相加函数,如果没有 friend 修饰则只许有一个右侧参数
friend String operate+( const String &s1, const String &s2);
private:
char *m_data;
}
String 的赋值函数 operate = 的实现如下:
String & String:: operate =(const String &other)
{
if(this == &other)
{
return *this;
}
delete[] m_date;
m_date = new char[strlen(other.m_date) + 1];
strcpy(m_date,other.m_date);
return *this;
}
String 的赋值函数,应当用“引用传递”的方式返回 String 对象。如果用“值传递”的方式,虽然功能仍然正确,但由于 return 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率
这赋值函数第一步:检查自赋值,可能你感觉多此一举,但是如果不对自赋值做处理,第二步:delete自己把自己杀了后还能复制自己吗?应马上终止函数,同时注意不要将if(this== &other) 错写成if(*this== other)
第二步:delete 这里要是不释放掉,以后就没机会了
第三步:分配新的内存资源,并复制字符串,注意函数 strlen返回的是有效字符函数 串长度,不包含结束符‘\0’ ,strcpy 则连‘\0’一起复制。
第四步:返回本对象的引用,那么能否写成 return other 呢?效果不是一样吗?
不可以!因为我们不知道参数 other 的生命期。有可能 other 是个临时对象,在赋值结束后它马上消失,那么 return other 返回的将是垃圾。
String 的相加函数 operate + 的实现如下:
String operate+(const String &s1, const String &s2)
{
String temp;
delete temp.data; // temp.data 是仅含‘\0’的字符串
temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
strcpy(temp.data, s1.data);
strcat(temp.data, s2.data);
return temp;
}
对于相加函数,应当用“值传递”的方式返回 String 对象。如果改用“引用传递” ,那么函数返回值是一个指向局部对象 temp 的“引用” 。由 于 temp 在函数结束时被自动销毁,将导致返回的“引用”无效
(5)内存指针的一些问题:
内存分配方式有三种:
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
计算内存容量
用运算符 sizeof 可以计算出数组的容量(字节数) 。sizeof("hello world")的值是 12(注意别忘了’\0’) 。指针 p 指向 a,但是 sizeof(p)的值却是 4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于 sizeof(char*),而不是 p 所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它
【1】:return 语句不可返回指向“栈内存”的“指针”或者“引用” ,因为该内存在函体结束时被自动销毁。例如
char * Func(void)
{
char str[] = “hello world”; // str 的内存位于栈上
…
return str; // 将导致错误
}
将上面的例子改成下面的,会怎么样?
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,它返回的始终是同一个“只读”的内存块。 如果str指向了这块静态存储区,str[0] = 'X'试图去修改静态存储区的内容就会导致错误
【2】指针操作超越了变量的作用范围。这种情况让人防不胜防
class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p 是“野指针”
}
(6)内联函数:提高函数的执行效率 (C程序里用宏代码提高执行效率)
但是C++为啥要不继续用宏代码,相比下内联函数有啥优点:C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的私有数据成员
C++ 的“函数内联”是如何工作的。对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型) 。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样) 。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。
inline 是一种“用于实现的关键字” ,而不是一种“用于声明的关键字”,即声明时可不加inline关键字,但是实现的时候必须加inline关键字
定义在类声明之中的成员函数将自动地成为内联函数,例如
class A
{
public:
void Foo(int x, int y) { … } // 自动地成为内联函数
}
内联的缺点:
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
大多是C/C++上的一些基础性问题,但是你深入探究还是会发现很多问题,这本书之前看过一遍然而再次看的时候还是收获不少,在此做个总结,希望自个对一些基础性问题能更加了解,加深印象