C++语言学习杂记

此杂记为学习过成功中的即时心得,不求精细,但求全面,具体知识点待其他文章中的专题讨论


任何情况下,定义一个CStyle的字符串常量,一定记得用const char*,例:
const char* psz = "Alex is a good programmer!!!";

对于字符串地址的指针变量,需要特别注意!!!往往一些函数在操作这些指针时,会取整个字符串,如果仅仅是取指针本身值,需要进行类型转换:
int i = 10;
int *p = &i;
cout << p; // 这里会输出i的地址,例如:0x002afef0

const char *pszName = "Alex";
cout << pszName; // 这里会输出整个字符串"Alex"
// 如果要输出p本身的值
cout << reinterpret_cast<const int*>(pszName);

===============================================
关于继承的访问权限:
1)不管何种方式继承,基类的保护成员和公有成员都可以在其第一层派生类内部直接使用(注意:仅仅是第一层派生类,后续的派生类中能否使用,取决于该派生类的最近的基类,即下述的第二点)
2) 对于多个层级的派生,一个派生类中的访问控制仅仅参照其最近的一个基类下发的派生方式
3)基类中的公有成员能否作为派生类的接口在外部调用,则必须是公有派生

================================================
类是一种数据类型,将其视为基本数据类型来考虑的话,可以扩展出很多知识点:
1)初始化:引出构造函数(默认构造函数和非默认构造函数)

================================================
关于const成员函数:
注意其对象本身也被视为const类型,所以若函数返回对象本身的地址或者引用,应该是const类型
class A
{
public:

// 返回常引用
const A& ConstTestOne(void) const {return *this;}

// 返回指针常量
const A* ConstTestTwo(void) const {return this;}
};

==================================================
对于内存中的数据,注意以下几个特性:
1、对于这种数据类型:
确定一种数据类型,即确定了4大特性:
a、有多大:数学意义上的大小
b、有多宽:占据多少个字节,即能表示的数据值范围
c、有多强:有哪些接口(类),可以进行哪些运算符操作
d、有多远:内存地址值(分配在哪种内存区域)
2、变量还是常量
3、何时被初始化
4、作用域、链接性和生存期(何时被销毁)

=================================================
类中的常量:
1)静态常量:(static const...)
类声明中的静态量只是一个标记,必须在类的实现文件中声明或初始化静态量。(但是static const int除外)
2) 枚举常量: enum{MAX_SIZE=128};类似于static const int
3) 非静态常量(还包括类型为引用的成员):(const ...)
a> 必须在所有版本的构造函数(包括拷贝构造函数)的初始化成员列表中初始化该常量(因为不允许出现未赋初始值的常量)
b> 必须为这个类定义显式的赋值运算符(因为默认的赋值运算符函数是按成员拷贝,会导致给常量成员赋值,而给常量赋值是非法的)

================================================
类设计原则:
1)一定要定义一个显式的默认构造函数,因为若定义了非默认构造函数,则隐式的默认构造函数自动消除,这样就不能创建建具有默认值的对象
2) 对于不修改对象成员的函数,一定写成const函数,方便常对象调用该函数
3) 如果函数返回一个对象的值,最好写成调用构造函数的形式,
这样的好处是若一个对象调用该函数进行初始化,可以避免生成临时对象(开销:临时对象的构造和析构、临时对象到目标对象的拷贝),而是直接调用构造函数初始化对象

================================================
由c++类的运算符重载函数的思考:(其实就是一种函数的重载)
1、内置的运算符重载:*、&、<<、>>
2、自定义数据类型的重载有更多的意义
3、其实全局函数就可以重载运算符,例如对自定义的结构体重载运算符号,但是对类而言无意义(因为不能访问类的私有成员)
4、因为运算符重载也是函数,其实不管内置的还是重载的运算符,其都可以视为是函数,只不过函数名是运算符号而已,
所以对于类的运算符重载也需要考虑是否定义为const函数的
5、用友元函数来重载运算符的主要目的是可以灵活设置操作数的顺序
6、对于自定义重载的运算符,可以写成运算符显式调用的形式如: --> operator+(obj1, obj2);
7、注意new, new[], delete, delete[]也是运算符,重载的函数原型如下:
void* operator new (size_t i){ return NULL; }
void* operator new[] (size_t i){ return NULL; }

void operator delete (void* pObj);
void operator delete[] (void* pObj);
一般不对上述4个运算符重载,而是让编译器自动生成这些函数的实现,编译器会控制堆空间的分配和销毁
8、不要混淆一元运算符重载和转换函数的书写区别:
// 重载一元运算符-
double operator - () { return -m_dfAmount; }

// 转换函数
operator double() { return -1; }

====================================================
运算符重载的限制:
1、只能用成员函数形式重载的运算符:=, [], (), ->
(注意:a.其中函数调用运算符参数不能为空;b.通过指针访问类成员运算符参数必须为空)
a、“函数调用”运算符的重载,运算符参数不能为空:
class A
{
// 参数不能为空
void operator () (int a, int b) { cout << "pass two integer number " << a << and << b << " to an object"; }
}

A a;
a(10, 20);

b、“通过指针访问类成员运算符”重载,运算符参数必须为空:
class A
{
// 参数必须为空
void operator -> () { cout << "Alex"; }
};
A a;
a.operator->();

2、重载不能改变运算符的操作数个数和优先级(结合性)
3、不能臆造出新的运算符(如*+)
4、必须有一个操作数参数是自定义类型(按值或者按引用传递,对于类类型,一般都是传递引用)

=====================================================
类和其他类型的转换:
类 --> 其他类型:转换函数 operator double() {return 1.0;},不过最好写一个显示转换函数,如ToDouble(){return 1.0},避免意外的转换
其他类型 --> 类:重载含有其他类型作为参数的构造函数 和 其他类型作为参数的复制运算符

=====================================================
关于多态性的几点:
当遇到基类的指针或者引用,必须联想到派生类的对象赋给这个指针或者引用时候的情况,
即如果基类指针或者引用调用了虚函数,则多态性体现,派生类对象则调用自己版本的虚函数













=====================================================
指定一个namespace在closing brace后面不需要加“;”,这点不同于类和结构体

关于void*指针:
void*指针只支持几种有限的操作:
1、把其他类型指针值赋给一个void*指针赋值
2、将void*指针强制转换为其他类型的指针
3、向函数传递void*指针或从函数返回void*指针
4、不允许使用void*指针操作它所指向的对象

注意函数指针的写法:
1、typedef 定义函数指针
typedef void (*TestPfn) (int nSize);
void Fn(int i) {++i;}
TestPfn tfn = Fn;
tfn(100);
2、更多时候,函数指针作为另一个函数的参数,连同另一个参数(函数指针所指函数调用的参数)
typedef void (*TestPfn) (int nSize);
int Pare(TestPfn pFn, int i) {pFn(i);}
3、函数指针的主要作用是实现“具有相同的参数列表的多个函数,在不同需要情况下的调用”

malloc(free)和new(delete)
1、内置类型和结构体一般用malloc(free)
2、类一般用new(delete)
3、free和delete之后需要将指针本身赋值为NULL
4、realloc和_msize的用法,例:
int* p = (int*)malloc(sizeof(int) * 8);
int* pNew = NULL;
if (p != NULL)
{
 for(int i=0; i<8; ++i)
 {
  p[i] = i;
 }
 pNew = (int*)realloc(p, sizeof(int) * 16);
 if (pNew != NULL)
 {
  for(int i=0; i<8; ++i)
  {
   cout << pNew[i] << endl;
  }

  cout << sizeof(pNew) << endl << sizeof(p) << endl; // 输出4(指针大小)
  cout << _msize(pNew) << endl << _msize(p) << endl; // 输出64 (指针所值堆大小)

  delete [] pNew;
  pNew = NULL;
 }
 else
 {
  delete [] p;
  p = NULL;
 }
}

面向对象编程的4大特性:
抽象 (公有接口)
封装 (数据隐藏)
继承 (派生类public, proteced,private继承基类的数据和接口)
多态 (基类和派生类通过虚函数,表现出“相同的接口不同的功能”)

面向过程:先考虑步骤,再考虑数据
面向对象:先考虑数据(如何表示数据),再考虑步骤(如何使用数据)
通常类的三种接口:初始化(构造函数),更新(抽象的接口),显示(重载输出运算符),回收资源(析构函数)

关于枚举,有两种用途:
1、创建符号常量
2、创建符号变量


在头文件中定义的函数一定要写成内联函数,否则多个cpp文件包含这个头文件时,会有链接错误,所以,
类声明中定义的函数都是内联函数,所以可以在任何.cpp文件中包含有类声明的.h文件

在头文件中不能定义变量,只能声明变量(如:extern int g_iNum),否则多个cpp文件包含这个头文件时,会有链接错误


所有类对象共享一个函数的副本,即多个对象执行同一个代码块;但成员变量不是

volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人
例:
volatile const int i = 10; // 若这里没有volatile,最后的语句在控制台输出的是10,而不是0
int* p = const_cast<int*>(&i);
*p = 0;
cout << i; //在控制台输出0

/*
拷贝字符串用strncpy_s(dest, dest_size, source, dest_size-1)
首先,当字符串超长的时候,会抛异常,这就起到了保护的作用。
一般具体用法如下:
当拷贝的字符个数等于source的长度或者等于dest_size-1,拷贝终止,若发现超长,则将dest置为空字符串
char szBuf[8];
strncpy_s(szBuf, 8, "AlexAlexAlexAlexAlexAlexAlexAlex", 15); // dest溢出,会抛异常,起到保护作用
strncpy_s(szBuf, 8, "Alex", 8-1); // 最后一个参数设置为8-1=7,保证不溢出
strncpy_s(szBuf, 8, "AlexAlexAlexAlexAlexAlexAlexAlex", 8-1); // 最后一个参数设置为8-1=7,保证不溢出(但是发生截断)

连接字符串用strncat_s(dest, dest_size, source, dest_size - 1 - dest_len)
首先,当字符串超长的时候,会抛异常,这就起到了保护的作用。
一般具体用法如下:
当连接的字符个数等于source的长度或者等于dest_size - 1 - dest_len,连接终止,若发现超长,则将dest置为空字符串
char szBuf[8] = "ab";
strncat_s(szBuf, 8, "AlexAlexAlexAlexAlexAlexAlexAlex", 15); // dest溢出,抛异常
strncat_s(szBuf, 8, "Alex", 8-1-strlen(szBuf)); // 最后一个参数设置为8-1-6=1,保证不溢出
strncat_s(szBuf, 8, "AlexAlexAlexAlexAlexAlexAlexAlex", 8-1-strlen(szBuf)); // 最后一个参数设置为8-1-6=1,保证不溢出(但是发生截断)

格式化字符串用_sntprintf_s:
TCHAR szBuf[16] = { 0 };
_snprintf_s(szBuf, 16, 100, _T("Alex is %f years old!!!"), 1.0f); // dest溢出,抛异常
_snprintf_s(szBuf, 16, 16-1, _T("Alex is %f years old!!!"), 1.0f); // 第三个参数设置16-1=15,保证不溢出
_snprintf_s(szBuf, 16, 16-1, _T("Peter is a programmer and is SDET, Nick is a SDET as well, alos, Alex is %f years old!!!"), 1.0f); // 第三个参数设置为16-1=15,保证不溢出(但是发生截断)

以上三个函数保证了:1)溢出的时候抛异常 2)dest字符串空间不足以装下整个source字符串时,产生截断

TCHAR szBuf[8] = { 0 };
_sntprintf_s(szBuf, 8, 8-1, _T("Alex is %f years old!!!"), 1.0f);


*/

类中的4大函数(四大金刚):
1、默认构造函数
2、拷贝构造函数
3、赋值运算符函数
4、析构函数
以上4个函数,在没有显式定义的时候,都会有隐式版本,但是特定的情况下,必须显式定义!

关于构造函数:
0、引出构造函数 ---> 类不同于内置数据类型和复合数据(指针、数组、结构体、共用体、枚举)类型,对象不能用简单的赋值初始化语句来初始化
1、仅仅在初始化对象的时候会自动调用,而对象赋值用的是赋值运算符
2、显式调用构造函数来初始化对象,有可能会生成临时对象(取决于具体实现)
3、最好或者说一定要提供显式的默认构造函数(哪怕函数体为空)

关于析构函数:
1、最好显式得写出类的析构函数,即使函数体为空
2、如果一个类有派生类,则必须将析构函数写成虚函数

类中的静态量:
1、准确的说,类的静态量(包括常量和变量)不是类的成员,它只是一个作用域在类中的静态存储区的一个量
2、类声明中的静态量只是一个标记,必须在类的实现文件中声明或初始化静态量,且不需要static关键字(但是static const int除外)


注意const成员函数和参数是const指针或引用:
1、const成员函数: 因为this指针是指向常量的指针,所以返回a.)某个成员的地址b.)某个成员的引用c.)对象本身(*this)的引用时也必须是const
2、参数是const指针或引用:当返回参数的指针或引用时,必须也是const
3、若指向常量的指针或指向常量的引用作为右值,左值也必须是相应类型的变量

关于成员函数:
1、写类接口(包括运算符重载)时候注意考虑是否需要const函数
2、当函数返回的是成员的地址或者引用时,若变量对象和常量对象都能调用该函数,则需要定义const和非const两个版本的函数


关于友元函数:
1、友元函数可以用来重载运算符函数,因为其可以直接访问私有成员
2、但是友元函数不是只能用来重载运算符,其函数的作用域中,可以定义指向类的对象、指针或者引用,然后在函数体中操作私有成员
3、可以定义两个类共同的友元函数

关于友元成员函数:
<1>定义顺序:
被调用类的前置声明
调用类的声明
被调用类的正式声明
<2>最好在单独的cpp文件中定义函数的实现,避开接口定义的顺序要求

关于友元类:
<1>包含单向友元和互为友元
<2>单向友元情况下类声明的顺序:
被调用类的声明
调用类的声明

关于类的自动类型转换:
1、类 -- > 其他类型:转换函数:如operator double () const;
2、其他类型 --> 类: 对于只有一个参数的构造函数,可以用隐式转换(最好用explicit禁止,否则容易发生语义错误)
3、注意一个参数的构造函数可以进行隐式转换的几种情况(见截图)和类的转换函数

关于多态:
1、派生类的虚函数若需要调用基类版本的虚函数,必须使用作用域解析运算符
2、虚析构函数 --> 保证正确的析构函数调用序列(派生类析构 --> 基类析构)

关于成员指针运算符:http://vipjy2008.blog.163.com/blog/static/372087672013933226346/
成员指针运算符例子:
/*
class TestClass
{
public:
 int m_i;

public:
 TestClass(int i) : m_i(i) {}
 void Fun() {cout << "Alex\n";}
};

int main(int argc, char* argv[])
{
 int TestClass::*pInt = &TestClass::m_i; // 定义一个整型数指针pInt(作用域在类TestClass中)
 void (TestClass::*pFn)() = &TestClass::Fun; // 定义一个函数指针pFn(作用域在类TestClass中)
 
 // .*运算符
 TestClass tc = TestClass(100);
 cout << tc.*pInt << endl;
 (tc.*pFn)();

 // ->*运算符
 TestClass* pNew = new TestClass(100);
 cout << pNew->*pInt << endl;
 (pNew->*pFn)();

 cout << "=================================\n";
 cout << "Press [Enter] key to quit\n";
 cin.get();

 return 0;
}
*/

生成随机数:
/*
#include <ctime>
using namespace std;
int ary[] = {1, 34, 5, 1, 6, 7, 9, 10};
int iIndex = 0;
srand(time(0));
for (int i = 0; i < 10; ++i)
{
 iIndex = rand() % (sizeof(ary) / sizeof(int));
 cout << ary[iIndex] << endl;
}
*/

生成不重复的随即数:
例:取数组中任意两个不重复的元素,将它们加入堆栈

#include <ctime>
#include <algorithm>
using namespace std;

const int SIZE_NAME = 32;
struct customer
{
 char szFullName[SIZE_NAME];
 double dfPayment;
};

struct customer cusAry[] =
{
 {"Alex", 23.5}, {"Peter", 23.5},
 {"Nick", 23.5}, {"Sheila", 23.5},
 {"Golf", 23.5}, {"Steven", 23.5}
};

stack<struct customer> sk = stack<struct customer>();
 
// pIndexAry动态数组,存放下标 (注:若上方数组cusAry指定了大小,这里不需要用动态数组)
int *pIndexAry = new int [sizeof(cusAry)/sizeof(struct customer)];
if (pIndexAry != NULL)
{
 for (int i=0; i<sizeof(cusAry)/sizeof(struct customer); ++i)
 {
  pIndexAry[i] = i;
 }

 srand((size_t)time(0));
 // 随机打乱pIndexAry动态数组的值(即下标的顺序被打乱)
 for(int i=0; i<sizeof(cusAry)/sizeof(struct customer); ++i)
 {
  swap(pIndexAry[0], pIndexAry[rand() % (sizeof(cusAry)/sizeof(struct customer))]);
 }
}

// 取前两个元素作为随机的2个元素
for (int i=0; i<2; ++i)
{
 sk.push(cusAry[pIndexAry[i]]);
}

if (pIndexAry != NULL)
{
 delete [] pIndexAry;
}

设计一个类:
必须做的:
1、显式定义默认构造函数

视情况而做的:
1、如果类中有动态分配的内存:
<1>、一定要显式定义析构函数,拷贝构造函数,赋值运算符函数
<2>、若派生类中需要动态分配内存,则不管基类中是否动态分配内存,都需要在派生类中显式定义析构函数,拷贝构造函数,赋值运算符函数

2、是否有非static常量(如const int m_i;)和引用类型的成员,若有:
<1> 在所有的构造函数中,必须在初始化成员类表里面初始化非static常量和引用类型的成员
<2> 必须显式定义赋值运算符函数(避开非static常量和引用类型的成员赋值操作)
例:
/*
class A
{
private:
 int& m_ref;
 const int m_i;
 float m_f;
public:
 A(int& i) :m_ref(i), m_i(0){}
 A& operator = (const A& a)
 {
  // 避开常量和引用的赋值
  m_f = a.m_f;
  return *this;
 }
};
*/
3、是否有必要定义为抽象基类
4、是否有静态量,若有,
<1> 必须在类的实现文件中初始化
<2> 考虑定义静态成员函数来操作或返回该静态量的值
5、是否需要重载多个构造函数,以增强构造对象的方法
6、是否需要重载一些运算符(如比较运算符,数组下标运算符等)
7、是否存在多态的函数,若有,则需要将其定义为虚函数
8、是否是有派生类的基类,若是,考虑定义虚析构函数
9、是否是派生类,还要注意:
<1> 最好重载一个“基类常引用+派生类特有成员”的构造函数,以显示派生类比基类多出的属性
<2> 在所有构造函数的初始化成员列表中,必须显式调用基类对应版本的构造函数
<3> 必须重写基类中的虚函数

关于重写虚函数的规则:
1、派生类中的虚函数返回值(除了类型协变)和特征标必须完全一样
2、若基类中虚函数被重载以致有多个版本,则派生类中必须重写所有版本

关于public, protected, private成员的访问权限:
1、从类外的角度,只有public成员可以直接访问,protected成员, private成员不能访问
2、从派生类(不管是public派生 or protected派生 or private派生)的角度,只有public成员, protected成员可以直接访问

关于public, protected, private派生方式,基类成员在派生类中对外访问属性的变化:
public派生:基类的所有成员均保持原来的访问属性
protected派生:基类的public变为protected,protected和private成员的属性不变
private派生:基类所有的成员均变成private访问属性

关于抽象基类:
1、引出抽象基类:基类中的属性和方法不一定全部适用于派生类,
故需要一个包含最通用属性和方法的模型(之所以说模型,是因为其不能实例化对象,例子:表示各种形状的图形,圆和椭圆)
2、实现方法:包含定义一个纯虚函数
3、属性:通用属性;方法:通用方法(非虚函数)和多态方法(虚函数)

关于对象之间的关系:
1、is-a: public 派生 - 继承了基类的(公有)接口
2、has-a: - 没有继承基类或包含对象的接口
<1> 保护派生,私有派生(隐式包含) - 包含“未命名对象”,多用dynamic_cast<>()类型转换访问被包含对象
<2> 对象包含(显式包含)  - 包含已命名对象,直接用对象名称访问被包含对象
<3> 一般情况,考虑使用对象包含(显式包含) ,若需要访问被包含对象的保护成员或重写虚函数,则采用保护派生,私有派生(隐式包含)
<4> 使得基类中的“公有成员”和“保护成员”在派生类中由不可见变为可见:采用using声明
例:
/*
class A
{
/*public:*/protected:
 int m_iA;
 void Test() {}
};

class B : private A
{
public:
 using A::Test;
 using A::m_iA;
};
*/

关于多重派生:
1、两个问题:
<1> 多个基类:
解决方案:采用虚继承
<2> 哪个方法(若基类有一虚函数,派生类1,派生类2中均重写,则多重派生类中无法识别究竟使用1中还是2中的版本)
解决方案:在多重派生类中重写基类的虚函数
2、注意点:
<1> 多重派生类的构造函数的初始化成员列表一定要有虚基类的构造函数
<2> 一般来说,一共可以重载5个版本的构造函数(包括显式默认构造函数),参照例class SingerWaiter...

关于类模板:
1、类模板本身不是类,必须实例化才可以算是一个类
由于上述原因:
<1>、如果把接口的实现代码单独放在cpp文件中,编译器是无法编译的,
正确的做法是将定义部分和实现部分均放在.h文件中,然后在使用模板实例化的.cpp文件中包含该头文件。
<2>、在模板外定义函数时,在所有表示类名的地方,一定要是模板名+参数列表
2、类型要和接口中的算法一致,比如赋值运算要用在支持赋值运算符的类型上才可以
3、模板参数可以有默认类型:与函数的默认参数规则一样,
当一个参数为默认类型时,其右边所有参数必须也是默认类型(包括表达式参数)
4、关于模板具体化:
// 帮助理解:第一个尖括号里面是没有被具体化的参数

// 通用模板
template <typename T1, typename T2, typename T3, typename T4>
class TTest
{};

// 模板(全部)具体化
template <>
class TTest <int, float, double, char>
{
};

// 模板部分具体化 (注意和函数模板的部分具体化写法的区别:函数模板的部分具体化无第二个尖括号)
template <typename T1, typename T4>
class TTest <T1, int, float, T4>
{};
5、模板的嵌套定义(模板中嵌套定义另一个模板)
6、模板的参数可以是另外一个子模板:
a) 子模板名写死在参数列表中(传递一个类型参数即可)
template <typename T1, typename T2 = TStack<T1>, typename T3 = OutTemplate<T1>>
class MyTTestParaIsT1
{
private:
 T1 m_t1;
 T2 m_t2;
 T3 m_t3;
};

MyTTestParaIsT1<int> av;

b) 子模板名可以传递,自由度更大(再传递这个子模板需要的类型参数)
template <template<typename T> class Thing, typename T2, typename T3>
class MyTTestParaIsT2
{
private:
 Thing<T2> m_a;
 Thing<T3> m_b;
};
MyTTestParaIsT2<TStack, int, float> bv;
7、模板友元函数的三种定义方式:
<1> 非模板函数:
<2> 非约束模板函数:
在模板类中声明并在模板类后定义

<3>在模板类前声明,在模板类中具体化,在模板类后定义

关于C语言的函数分别在C和C++中编译并且调用:
1、用一个.h文件来声明函数:
/*
#ifdef __cplusplus

extern "C"
{
#endif

void CFunctionTest(void);

#ifdef __cplusplus
}
#endif

*/
2、在.C文件中包含该.h文件并定义该函数,此时没有定义__cplusplus
3、在.cpp文件中包含该.h文件并调用该函数,此时已经定义__cplusplus

// 忽略指定的警告
#pragma warning( disable : 4298 )

// 关于异常
1、关于throw:
throw的目的既是找到匹配的catch处理块,其间会有堆栈退解
throw的位置:
<1>throw若在try中,则先从该try对应的catch中寻找匹配的处理程序;若无匹配,在函数外try对应的catch中寻找
<2>throw若在catch中或若在一般语句中,直接在函数外try对应的catch中寻找
<3>在catch块中若调用throw;,表示再次抛处这个异常,继续堆栈退解
<3>若throw最终找不到匹配的处理程序,则abort退出
2、一个try可以试图对应多个catch
3、通常使用异常类来表示异常的具体信息
<1>异常类的派生和捕捉处理程序的顺序(相反:目的是为了能够让每一种异常都严格匹配各自对应的异常类)
void Fn()
{
 if (***)
 {
  throw A();
 }
 else if (***)
 {
  throw B();
 }
 else if (***)
 {
  throw C();
 }
 else if (***)
 {
  throw D();
 }
}

void main()
{
 try
 {
  Fn();
 }
 catch(D& d)
 {
  cerr << d.what();
 }
 catch(C& c)
 {
  cerr << c.what();
 }
 catch(B& b)
 {
  cerr << b.what();
 }
 catch(A& a)
 {
  cerr << a.what();
 }
 catch(...) // 捕获所有异常
 {
  cerr << "出现异常!!!";
 }
}
<2>写自己的异常类:通常选择从exception、logic_error、runtime_error派生:
a、用一常静态指针变量指向描述特定异常信息的字符串(static const char* m_pMsg)
b、重写what()虚函数
4、一函数中,若堆栈退解时会跳过一些必须的步骤(比如释放堆内存),可以在该函数中捕捉该异常,仅仅用来执行必须的步骤,
然后在catch块中再次throw。例:
int Devide(const int i, const int j)
{
 int* pNew = new int [128];

 try
 {
  if (j == 0)
  {
   throw DevidedByZero(i);
  }
  else
  {
   if (pNew)
   {
    delete [] pNew;
    pNew = NULL;
   }

   return i / j;
  }
 }
 catch(DevidedByZero& dbz) // 在该函数中捕捉该异常,释放堆内存
 {
  if (pNew)
  {
   delete [] pNew;
   pNew = NULL;
  }

  throw dbz; // 再次抛出该异常,在函数外被捕捉并处理
 }
}

关于dynamic_cast<pionter* or reference>(pionter* or reference)运算符
1、基类中必须定义虚函数(这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。)
2、操作数必须是指针或者引用,转换时,要根据圆括号中的指针或引用实际指向的对象类型来判断,而不是指针和引用自己的类型
3、当为引用时,需要试图捕捉bad_cast异常来判断是否成功转换
4、该运算符主要用于判断对象是否是某一个类或其派生类类型:
例:
class A
{
public:
 virtual void Show(void) const {cout << "Show Class A";}
};

class B : public A
{
public:
 virtual void Show(void) const {cout << "Show Class B";}
};

class C : public B
{
public:
 virtual void Show(void) const {cout << "Show Class C";}
};

int main(int argc, char* argv[])
{
 A* pA = NULL;
 B* pB = NULL;

 // 适时创建各种类型对象,并用pA指向
 if (...)
 {
  pA = new A();
 }
 else if (...)
 {
  pA = new B();
 }
 else
 {
  pA = new C();
 }
 
 if (pB = dynamic_cast<B*>(pA)) // 当对象是B或C类型时
 {
  pA->Show();
 }
}

关于typeid()操作符:
1、操作数只能是类名和结果为对象(或引用)的表达式
2、返回一个type_info结构,该结构重载了==和!=运算符,故可以用来比较
<1> 一个对象(或引用)是否是指定类型
<2> 两个对象(或引用)是否是相同类型

关于const_cast<pionter* or reference>(pionter* or reference)操作符:
1、该运算符的作用主要是删除指针或者引用的const属性
2、操作数必须是指针或者引用
3、注意:由c++标准,改变const属性对于不同的编译器,表现是不一样的,
对于VS,一般采用优化,若不想优化,可以定义个volatile const变量

关于static_cast<type-name>(expression)运算符:
1、用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的互相转换
<1> 派生类指针或引用 -->(赋值给) 基类指针或引用
<2> 基类指针或引用 -->(赋值给) 派生类指针或引用:有风险
2、用于基本数据类型之间的转换
3、把void*指针转换成目标类型的指针

// cin可以返回bool值的原因所在:转换函数
operator void *() const { if(state&(badbit|failbit) ) return 0; return (void this; }

=========================================
其他数据类型和string类型的互相转换:
#include <sstream>
using namespace std;
int main(int argc, char* argv[])
{
 // 其他数据类型 ====> string类型
 int i = 10;
 float f = 10.0f;
 double df = 10.0;
 string str = "Alex";
 string strDest;

 ostringstream oss; // 定义string输出流对象

 // 向string输出流对象发送数值,准备构建string对象
 oss << i << '\t' << f << '\t'
  << df << '\t' << str;
 
 // 关联到string对象
 strDest = oss.str();
 
 ===============================================================================
 
 // string类型 ====> 其他数据类型
 string str = "10\t20\t30\t40";

 int i;
 istringstream iss = istringstream(str); // 将str绑定istringstream,视为输入流

 // 依次从输入流抽取字符,转换为整形
 while (iss >> i)
 {
  cout << i << '\t';
 }

 string strWord;
 // 依次从输入流抽取字符,转换为string
 while (getline(iss, strWord, '\t'))
 {
  cout << strWord << '\t';
 }
}

===============================
关于文本文件按照行读写:
#include <fstream>
using namespace std;

// 按照行读取文件In.txt,并将其写入Out.txt
ifstream fin;
ofstream fout;
string strLine;

fin.open("In.txt");
if (!fin.is_open())
{
 cerr << "Fail to open file. Quit.";
 exit(EXIT_FAILURE);
}

fout.open("Out.txt");
if (!fout.is_open())
{
 cerr << "Fail to open file. Quit.";
 exit(EXIT_FAILURE);
}

while (getline(fin, strLine, '\n'))
{
 fout << strLine << endl;
}

// 若用CStyle数组来读取:这种方法可能会造成行字符串的截断(不推荐)
/*
while (!fin.getline(szBuf, 8, '\n').eof())
{
 if (!fin) // 若行的长度大于数组长度
 {
  fin.clear();
 }
 cout << szBuf << endl;
}
*/

fin.close();
fout.close();

if (fin.eof())
{
 fin.clear();
}

关于string类:
1、多重载版本的构造函数可以在初始化对象时就可以多样化构造对象:
<1> 指定C风格字符串的区间
<2> 指定个数的单个字符
<3> 指定另一个string对象的区间
2、string对象和char*的互相转换
<1> char* --> string: 通过string的构造函数
<2> string --> char*: 通过string的的接口函数c_str()
3、string类提供和很多关于字符串操作的接口:
合并,比较,删除,插入,查找子串(正向,反向),
4、当函数参数为const string&时,考虑重载一个类型为const char*的参数,可以避免类型转换造成的低效率

关于STL容器:
1、所有容器都有的接口:empty(), size(), end(), begin(), iterator迭代器的使用
2、部分容器(例如:vector类模板)有的接口:
<1> push_back(): 末尾加一元素
<2> erase(begin, end): 注意区间左闭右开
<3> insert:理解为一个车队加塞(我的位置, 加塞车队的车头位置, 加塞车队的车尾位置+1)
<4> swap:交换两个容器中的元素
3、通用函数(STL)模板:
<1> 遍历:for_each(begin(), end(), 具有元素(常)引用类型参数的函数)
<2> 查找:find(begin(), end(), 元素的对象或者引用);
返回值:该函数返回一个迭代器,若找到,则为该元素对应的迭代器位置;若没有找到,返回超尾的迭代器位置
要求:元素的类型必须支持==运算符
<3> 拷贝:copy(begin(), end(), 目标位置迭代器或输出迭代器或插入迭代器)
<3> 排序:容器必须支持随机访问
版本A: sort(begin(), end()),容器的元素类型必须支持小于运算符(<),如果没有,必须重载<运算符
版本B: sort(begin(), end(), 返回值是bool、参数是两个元素(常)引用类型参数的函数<自定义算法比较大小>)

关于迭代器:
1、可以是容器类模板中定义的一个嵌套类,重载了例如*、++、--、==、!=等运算符号()
2、亦可以是容器中数据元素的一个指针(例如容器vector)
3、亦可以是一些独立于容器的类模板:如ostream_iterator, istream_iterator, insert_iterator
例:
注:ostream_iterator的模板类型参数是容器中元素的类型
ostream_iterator<double, char> out = ostream_iterator<double, char>(cout, " ");
注:insert_iterator的模板类型参数是容器的类型,s3是目的容器
insert_iterator< set<double> > out1 = insert_iterator< set<double> >(s3, s3.begin());

4、指针可以作为一种特殊的迭代器,包含一切迭代器的特性(比如可以把数组元素的地址传递给STL函数)

关于STL函数:
1、其本身是一个函数模板,实例化时传递的实参一般是迭代器(或指针)
2、STL函数的算法决定使用的迭代器类型,而迭代器类型又进一步确定了其适用的容器类型,从而决定了何种算法函数适用于何种容器
3、容器、迭代器、容器中的元素三者都是数据结构,STL函数通过操作迭代器来操作容器中的元素,最后达到操作容器的目的

函数符:
意义:作为一个STL函数模板的参数传递
1、函数名
2、函数指针
3、重载了()运算符的类模板对象:
<1> 自定义类模板
template <typename T>
class MultiItem
{
private:
 T m_t; // 内部操作数字,比如被加数,被乘数等等,自定义创建

public:
 void operator () (T& t) {t *= m_t;} // 重载()运算符,STL函数实例化时,参数t指向容器元素的引用
};
<2> #include<functional> 中包含的预定义函数符(类模板)

vector:优点->在尾部添加和删除元素
deque: 优点->也是一种vector,支持在首部快速添加和删除元素
list: 优点->在任何一个位置删除和插入的时间复杂度都是线性的

set(multiset)和map(multimap):
1、multi - 表示可以多个关键字
2、可以传递第二个模板参数给函数符来指定排序方式,默认是从小到大排序
multimap使用范例:
#include <map>
using namespace std;
multimap<string, string> mapCode = multimap<string, string>();
mapCode.insert(pair<const string, string>("Alex1", "Wu1"));
mapCode.insert(pair<const string, string>("Alex2", "Wu2"));
mapCode.insert(pair<const string, string>("Alex3", "Wu3"));
mapCode.insert(pair<const string, string>("Alex4", "Wu4"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex5", "Wu5"));
mapCode.insert(pair<const string, string>("Alex6", "Wu6"));
mapCode.insert(pair<const string, string>("Alex7", "Wu7"));
mapCode.insert(pair<const string, string>("Alex8", "Wu8"));

multimap<string, string>::iterator itMap;
for (itMap=mapCode.begin(); itMap!=mapCode.end(); ++itMap)
{
 cout << (*itMap).first << "    " << (*itMap).second << endl;
}

pair< multimap<string, string>::iterator, multimap<string, string>::iterator > range =
 mapCode.equal_range("Alex5");

cout << "Count: " << mapCode.count("Alex5") << endl;
for (itMap=range.first; itMap!=range.second; ++itMap)
{
 cout << (*itMap).second << "    " << endl;
}

copy和transform

注意ostream_iterator作为输入迭代器来输出自定义类型的时候,需要在std namespace中重载<<运算符!!!:
例:
// 自定义输出pair<const string, int>类型
namespace std
{
 ostream& operator << (ostream& om, const pair<const string, int>& item)
 {
  om << item.first << endl
    << item.second << endl;

  return om;
 }
}

front_insert_iterator和insert_iterator的区别:
front_insert_iterator插入后,对于源是反向排列;insert_iterator插入后,对于源是正向排列;

========================
10/16
C和C++区别:
1、C中,省略函数返回值可以理解为返回值为整形,但是C++中会编译错误
2、C中,函数参数列表为空,表示可以传递任何参数,但是C++中会编译错误;C++中参数列表为空和显式写void等效

结构化编程:数据类型、运算符、控制结构,侧重算法(设计出满足算法要求的程序)
面向对象编程:封装和数据隐藏、继承,多态,侧重数据(设计出反应问题本质特性的数据类型)
通用性编程:忽略数据类型,侧重通用方法解决同类问题

C++的可移植性(可移植性:相同的源代码,在不同平台的编译后,仍旧可以运行)的需求,引出了必须需要一个标准来规范C++,即ISO/ANSI C++

生成可执行程序过程:源代码编译成目标代码-->链接库代码、启动代码-->生成可执行文件

分清输入和输出:
输入:数据从输入设备或者文件进入程序 >>
输出:数据从程序流入输出设备或者文件 <<
标准输入流,文件输入流
标准输出流,文件输出流

头文件中的元素可以包含在不同实现文件中,随会重复出现,但不会产生错误,头文件中可以有的元素:
首先注意防止头文件被重复包含:#ifndef ... #define ... #endif
自定义数据类型的声明(枚举、结构、类)
常量的定义(宏形式或者const形式)
非内联函数的声明(函数原型)
内联函数的定义

老式的C语言头文件在C++中被定义为cmath、cstdio等名称

名称空间的使用:使得不同模块的元素(数据类型、变量,常量、函数等)区分使用,注意和类作用域运算符使用的区别
三种使用名称空间元素的方法:
1、using namespace std (using编译指令)
2、using std::cout (using声明)
3、std::cout << 100; (作用域运算符)

内置的重载操作符:+, -, *, &, <<, >>

函数指针也可以作为一种数据类型,例如
void Test(int (*pFN) (int, int));

语句中标记和空白的概念

程序 --> 模块 --> 类 --> 函数 --> 语句:
1、表达式语句:运算符 + 操作数(变量、常量、表达式)
2、声明语句(可以初始化)
3、函数调用语句
4、函数声明语句
5、空语句

对于变量的理解:
1、有多远
2、有多宽
3、有多强
4、有多少

数据类型有内置类型和类类型:
1、内置类型:基本数据类型和复合类型(指针、数组、字符串、结构体、枚举)
2、类类型(类库和自定义类)

char, short, int, long四种整型数以及各自无符号版本的:
1、表示范围
2、占据的字节数
3、整型数之间的赋值要注意范围是否合适,是否有截断

整型数常量的表示方法,L和UL后缀,整型数常量的类型确定法则(十进制整型数和非十进制整型数不一样)

ASCII码值在0到31范围的均为控制字符,一般用转义序列的形式表示
转义序列:
1、固定符号转义序列:\n, \t, \v, \b, \r, \a, \\, \?, \', \"
2、通用数字转义序列,用2位八进制或者2位十六进制,可以表示所有字符:如\xA, \x41

bool型和整型数之间的互相转换

const初始化常量相对宏定义好处:
1、可以指明数据类型
2、具有作用域属性
3、除了用于内置数据类型,还可以用于自定义类型

单精度float的有效数字位数:6~7,范围:10E-38~10E+38

浮点数常量,默认是double类型,可以加后缀设置存储方式:
float f1 = 1.0F;
long double f2 = 1.0L;

单精度浮点数输出:
1、默认情况下,只显示6位数字,即使有小数部分,仍然不显示
2、大于等于百万级别的数,将采用科学计数法显示,尾数部分显示6位
3、cout.setf(ios_base::fixed) 关闭科学计数法显示,强制显示小数点后面6位

数据类型转换的几种情况:
1、赋值转换
2、表达式中操作数类型转换
3、函数调用时参数类型转换

数值类型之间的转换主要从取值范围和精度方面考虑

数值之间的强制类型转换,最好用static_cast<>() 运算符

多个字符串常量之间可以用WS拼接

对于CStyle字符串数组,使用cin>>输入字符串的缺点:
1、不能识别WS
2、不能处理超长的情况
3、最好使用cin.getline,可以识别WS

关于公用体:
1、存储空间的大小取占有字节数最大的数据类型
2、数据类型的选择可以实时切换
3、如果数据类型取错,则取值为乱码
4、如果是嵌套在结构体中的公用体,在声明的时候采取“前有后有,前无后无的”书写规则:

// 公用体类型定义(嵌套于结构体中)
struct STUDENT
{
 char szName[128];
 int iAge;
 int iType; // 决定下面ID的数据类型
 
 union
 {
  int iID;
  char szID[16];
 };
 或者
 union ID
 {
  int iID;
  char szID[16];
 }id_val;
};



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值