Posts Tagged C/C++

Member functions with the const constraint in C++

一、const成员函数的限制

在设计类时,有时为了保证某个成员函数不会修改成员变量,将其标志为const。

	public:
		void set() const;

比如这里的set函数不能修改成员变量的值,否则会产生一个编译错误。

但是有一种情况除外:目标成员变量是static

即,const成员函数可以不受限制的修改static成员变量的值

	public:
		void set() const
		{
			n = 1024;
		}

	int test::n;

	private:
		static int n;

这样的代码是合法的。

这种做法看上去很好,因为他解决了const成员函数有可能需要修改极个别变量的问题。但是在更多的时候,把成员变量标记为static不是一种值得推荐的行为。

为了使某些成员变量能够被const成员函数修改且避免把自己标记为static,C++提供了mutable关键字。

mutable修饰的变量表示可变变量,该变量允许const成员函数修改它的值。

	public:
		void set() const
		{
			n = 23;
		}

	private:
		mutable int n;

二、成员函数的const重载规则

一个经常被人忽略的规则是:即使两个成员函数除了const之外没有任何区别(即一个是const成员函数,另一个不是),这两个成员函数的重载也是合法的

一个存在极其拙劣的设计但是仅用于说理的例子如下:
Read the rest of this entry »

, , ,

1 Comment

Effective C++笔记:C++组成及类内部常量的实现

一、为什么C++这么复杂

自从C++发展成熟至今,对其过于复杂的抱怨不绝于耳。C++为什么如此复杂?

在很久之前,为了应对OO思想带来的冲击,C++作为C在OO特性上的改进版出现在人们视野之中。而C++最初的名字,也恰好是C with classes。

渐渐发展和成熟后的C++,吸收和借鉴了众多新式的编程思想和特性,也变得愈加复杂,最终成为不同于C的另一种语言。

Scott Meyers认为,现在的C++应看作多种语言的集合,而不是单独的某一种语言
。所以C++大致上可以分为四种子语言:

1.C:归根究底,C++仍然以C为基础。二者具有近乎一致的语法结构,预处理功能,内建数据类型,数组,指针 .etc,C几乎是作为一个完全被兼容支持的子集存在于C++之中。但C++在这个基础之上,也增加了诸如重载、异常处理等特性。

2.Object-Oriented C++:这可以说是C++最初的雏形。支持类,封装,继承,多态,虚函数等来自面向对象思想的语言特性。

3.Template C++:这部分通常称为泛型编程(generic programming)或者模板元编程(template metaprogramming)。模板的引入几乎使旧有的C++体系发生了根本性的变化,并且产生了和旧有截然不同的编程思维和方式。

4.STL:准确的说,STL是一个标准模板库。但它是一个很特殊的模板库(标准化的思想最初来自于一个法国的数学流派)。STL定义了自己特有的容器,迭代器,算法和函数对象,并且推荐使用这些东西来替代旧式的工具。而且这些由STL定义的东西,在STL的标准协议约束下,啮合的超乎想象。

C++之所以复杂,是因为你需要时常在几个子语言之间来回切换。而每个子语言,都自己的一套规则和约定,规则和约定间不可避免的会存在一些鸿沟。因而,你会看到许许多多游离在规则之间的特例。

不单单是程序员,有时候连编译器都不知道应该使用哪一套方法来处理代码。因而,当你发现编译器的做法和你想象的大相径庭时,不必感到奇怪,因为这并不罕见。

但是在事实上,很不幸的是,有许多号称自己通晓C++的C++程序员,往往停留在只知道1和2的水平上。他们不会使用甚至几乎不知道template和STL。更有甚者,完全把C++当作C来使用。

如果某家公司聘用这些人来开发产品,那将是一件很恐怖的事情。
Read the rest of this entry »

, , , , ,

3 Comments

表达式求解的一个简单实现

RT

好久没写Blog,写一篇凑凑数。

来自某个很无聊的“作业”,最初是让写一个简易科学计算器,为了避免做UI的麻烦,直接选择表达式求解的方式实现。

算法原型来自Lieo,经过部分改进后,支持负数和多重括号。

因为只是expression-evaluate的一个demo,所以没有写得太详细。

不支持小数,没有异常处理,包括除数为零。

这里说下算法:

首先需要有两个堆栈或者支持FILO性质的线性表(我用的是vector,便于调试时观察内部元素是否正确),分别保存操作数和操作符。

负数通过额外的符号位实现,并规定,操作符后面跟着的“-”表示负号,符号位逻辑求反。

遍历表达式时,如果碰到操作数,直接入操作数栈。碰到操作符op,先和操作符栈顶的元素比较优先级,如果优先级大于栈顶元素,入栈(如果操作符栈为空,直接入栈),否则取出操作符栈顶元素和操作数进行计算,一直到操作符栈空或者栈顶元素优先级小于操作符op。

对于括号特殊处理:

碰到“(”,直接入栈,碰到“)”,取出操作符栈和操作数栈计算,直到操作符栈中弹出“(”。

为了简化算法,表达式尾部需要有#符号充当哨兵元素,并规定#符号的优先级最小。

提供核心代码,全部代码请翻看SRC

#define pop(x) x.back(); \
				x.pop_back();

struct NumData
{
	NumData() : signal(false), pos(0)
	{
		wmemset(sz, L'\0', NUM_LEN);
	}

	enum{NUM_LEN = 10};
	wchar_t sz[NUM_LEN];
	bool signal;
	unsigned int pos;
};

int EvaluateExpression(wchar_t* pszExpression)
{
	stackOperand oprand;
	stackOperator oprator;
	bool isNumbericMode = true;
	NumData data;

	for (wchar_t* p = pszExpression; *p != NULL; ++p)
	{
		if (*p >= L'0' && *p <= L'9')
		{
			data.sz[data.pos++] = *p;
			isNumbericMode = false;
		}
		else if (L'-' == *p && isNumbericMode)
		{
			data.signal = !data.signal;
		}
		else	// pure operators
		{
			isNumbericMode = true;

			if (L'(' == *p)
			{
				oprator.push_back(*p);
			}
			else if (L')' == *p)
			{
				int num;

				if (data.pos)
				{
					num = FormatNumtoInt(data);
				}
				else
				{
					num = pop(oprand);
				}

				do
				{
					wchar_t op = pop(oprator);

					if (L'(' == op)
					{
						oprand.push_back(num);
						break;
					}
					else
					{
						int num2 = pop(oprand);
						num = Calculate(num2, num, op);
					}

				} while (true);
			}
			else	// operators except ( and )
			{
				int num;

				if (data.pos)
				{
					num = FormatNumtoInt(data);
				}
				else
				{
					num = pop(oprand);
				}

				do
				{
					if (oprator.empty() || (GetPriority(*p) > GetPriority(oprator.back())))
					{
						oprand.push_back(num);
						oprator.push_back(*p);

						break;
					}
					else
					{
						int num2 = pop(oprand);
						wchar_t op = pop(oprator);

						num = Calculate(num2, num, op);
					}

				} while (true);
			}

			//DumpStack(oprand);
		}
	}

	return oprand.back();
}

int FormatNumtoInt(NumData& data)
{
	assert(data.pos);

	int num = _wtoi(data.sz);
	num = data.signal ? -num : num;

	wmemset(data.sz, L'\0', NumData::NUM_LEN);
	data.pos = 0;
	data.signal = false;

	return num;
}

Download EvaluateExpression SRC

, , , ,

1 Comment

经过重写的托盘图标CTray类

重写CTray缘于最近在写的某个Project。此Project需要显示托盘,所以我自然而然的想到了重用以前写的CTray。

但是当我把CTray文件添加到工程时,发现之前在编写CTray时,考虑得不够周到,整个类设计的也有问题

比如:强制使用带参数的constructor、成员函数命名不规范和异常情况考虑不周等问题

于是打算将CTray重写一遍,由于前后风格(命名风格请参考:匈牙利命名法的衰落和建议)和设计思维的改变,原有的代码几乎被推倒重写了一遍。

嗯,于是写了一天,终于完成。

现在的头文件大概是酱紫的

/**************************************************

** Project:CTray Design

** File:Tray.h

** Edition:v1.0.0 Demo

** Coder:KingsamChen [MDSA Group]

** Last Modify:2010-5-3

**************************************************/

#pragma once

#define WM_MY_TRAY  (WM_USER + 10)

class CTray : public CObject
{
	public:
		CTray();
		CTray(UINT trayId, UINT message);
		virtual	~CTray();

	public:
		LRESULT OnTrayProc(WPARAM wParam, LPARAM lParam);
		BOOL Create(CWnd* pWnd, HICON hIcon);
		BOOL Create(UINT trayId, UINT message, CWnd* pWnd, HICON hIcon);
		BOOL Show(LPCTSTR pszTipText);
		void Delete();
		BOOL Change(HICON hIcon, LPCTSTR pszTipText);
		BOOL ShowBalloonTip(LPCTSTR pszInfoTitle, LPCTSTR pszInfo,
							UINT timeout, DWORD infoFlags);
		virtual void AssertValid() const;

	public:
		static UINT ms_taskbarCreateMsg;

	protected:
		NOTIFYICONDATA m_Nid;
};

唔,做几点说明吧:

1.显然,这个类还是为MFC设计的(WTL似乎也可以经过少量修改后直接使用),纯SDK的话需要大规模重写( ̄ε(# ̄)

2.支持原来的功能:显示/修改/删除图标,集成Message Proc,支持任务栏重建时恢复,气泡风格

3.原有接口几乎全被破坏- -||,所以就有代码需要更新。

4.另外,重写时翻文档发现了一点东西:

WinVista后,由于Tray Icon的UI做了大量修改,很多属性都不推荐使用,比如NOTIFYICONDATA.uTimeout。并且新增了一些成员(包括之前文档标识为Reserved)。

气泡弹出的效果也做了改进(不使用uTimeout,转而使用其他新增加成员)

但是考虑到功能统一,并没有使用新增加的效果,有兴趣的自己修改CTray

PS:新文档参考:http://msdn.microsoft.com/en-us/library/bb762159%28VS.85%29.aspx

新版本的CTray下载地址:http://cid-cb24c6c6cbd98991.office.live.com/self.aspx/Blog%20File%20Storage/CTray%20Design.zip

, , , , ,

No Comments

非常规排序问题

让问题飞一会儿1

考虑如下问题:

描述:假设有一个包含最多n个正整数的文件,每个数均小于n,此处n=10^7.且每个数字至多只出现一次。

目标:设计一个算法,将文件中的正整数按升序排列输出。

条件:RAM大约在1MB,最大占用不超过1.5MB2,运行时间在10S内,无磁盘空间约束。

这个问题抽象自某个数据库系统对美国800免费电话的排序问题:

当时美国免费电话由区号800加7位数字构成。系统在某些时候需要对这些号码进行排序。

对这个问题认真思考后,我们来考虑一些由这个原问题得到的扩展问题。

 

扩展问题

Q1:为了解决原问题,需要相应测试数据。我们需要一个算法,该算法能够产生[0,n)间不重复的含有K个(0<K<=n)元素的整数序列.

Q2:原问题中,RAM最大的空间是1.5MB,若提供的空间严格的只有1MB,对应的算法应该怎样修改?

Q3:如果数字允许最多重复出现10次,算法又该如何改进?

Q4:现在美国免费电话的区号有:800、877和888。而且还有不断增加的趋势,如何用1MB内存对上述号码进行排序?

Q5:在Q4的条件下(不包含1MB内存约束条件),如何将这些号码保存在一个集合中,实现对给定号码的快速查找?

Q6:在大空间中,初始化需要消耗大量时间。设计算法避免初始化。新算法的访问时间复杂度应该是O(1),且额外空间大小正比于排序数据大小。

 

原题分析

1.如果不考虑内存约束条件,即可以将10^7个整数全部读到内存中,在进行排序(比如QuickSort)。但是这个需要10^7 / 1024 / 1024 × 4 = 38MB。这个和允许的内存空间差距太大了。(感谢LittleHorse的debug :-)

2.如果采用基于磁盘文件的归并排序,则可以有效降低内存占用。

我们设一次排序K个数,那么有 K * 4 / 20^20 = 1.5 => K = 393216

我们可以一次读入35W个数据进行排序(内存中可以用QuickSort),得到输出文件后再利用MergeSort进行合并。

但是很不幸的,我们需要N = 10^7 / K = 29次文件I/O。

是否有算法既能降低内存要求又能避免大量的磁盘I/O?

3.回忆线性排序中的计数排序(CountSort),结合实际情况,我们考虑采用位向量(bitset)来表示数据。

bitset表示法即:假设v是一个位向量,用于表示非负整数。那么若第K个数存在,则v[k]为true,否则为false。

易知,1000W个数需要1000W bits = 1.2MB3,内存符合要求。
Read the rest of this entry »

, , , , ,

6 Comments

二叉查找树及一个封装拙劣的BST类

二叉查找树(Binary Search Tree, BST)是一种特殊的二叉树,常常用来作为进行数据查找的存储结构。

对于长度为n的线性表来说,搜索一个数据的时间是O(n),而对于BST,搜索的运行时间复杂度和高度成正比。

由二叉树的性质可以知道,当二叉树比较平衡时(比如完全二叉树),其高度h近似等于logn,n为结点数。所以,较为平衡的BST的搜索时间复杂度可以达到O(logn)。

 

1. 二叉查找树(BST)

BST通常用链表表示,每个结点至少会包含:parent,left,right和key等附加的数据域。如果孩子结点或者父结点不存在,则用NULL表示。

易知,树根结点是唯一一个父结点为NULL的结点。

BST有一个如下所述的重要存储性质:

在一个BST中,对任何结点x,其左子树中的key不大于x的key,其右子树中的key不小于x的key。即

如图为一棵BST

根据BST的性质可以知道,如果对BST进行中序遍历(Inorder Traverse),可以将BST中所有元素按有序输出

遍历一棵含有n个结点的BST所需要的时间复杂度是O(n),因为每个结点都会被访问。

 

2. BST类的设计

可以利用C++将BST封装成一个类,便于以后调用。且为了使得这个类通用,我们将类写成模板类

我们需要一个Node结构和一个控制结构的类CBinSearch。

为了尽可能地遵循封装思想,我们将Node类写在CBinSearch类的protected成员中,这样Node结构对外界是透明的。

考虑到某些函数要求客户传入一个指向Node结点的指针,我们仿照STL的iterator,定义出两个公共的结点指针类型

typedef  Node* pos;
typedef const Node* const_pos;

所以,最后我们的类声明大概是这样的
Read the rest of this entry »

, , , , , ,

8 Comments

The process in windows

1.进程属性

1.1.进程概述

在Win中,进程是正在运行程序的一个实例,包括一个内核对象和一个地址空间。

操作系统(Operating System,OS)用内核对象来管理进程及保存进程的统计信息。而在地址空间内,包含所有可执行文件或DLL模块的代码和数据及线程堆栈和分配的堆。

在Win中,进程本身没有执行代码的能力,需要依靠进程内的线程来完成。当OS创建一个进程时,会自动为进程创建一个线程,成为主线程(Primary-thread)。主线程可以创建更多的线程。每个线程都有自己的寄存器和堆栈。所有的线程都在进程的地址空间中“同时”执行代码。

如果进程中没有线程执行代码,OS将自动销毁进程并进行相关清理工作。

系统会轮流为每个要运行的线程调度一些CPU时间,使得看上去所有线程都在并发运行。

1.2.进程实例句柄

每个被加载到进程地址空间的EXE或DLL文件都被分配了一个唯一的实例句柄(HINSTANCE)。EXE文件的实例句柄以入口函数(w)WinMain的参数hInstanceExe传入。

PS:在非16Bit的Win中,HINSTANCE和HMODULE完全相同,可以互相代替使用。个人猜测和长短指针有关。

hInstanceExe参数表示OS将可执行文件映像加载到进程地址空间的某个内存基地址。基地址的选择(默认)由连接器决定。

为了得到一个EXE或者DLL文件的内存基地址,可以通过调用GetModuleHandle函数。

如果在DLL中想要得到调用DLL进程的基地址,可以调用GetModuleHandleEx,并传入GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS

1.3.进程命令行

OS在创建一个进程时,会传递一个命令行,包含附加启动信息(程序启动参数)

在一个WinGUI程序中,可以调用GetCommandLine来获取进程完整的命令行。CUI程序的命令行大多保存在argv[]中。

对于WinGUI程序,调用CommandLineToArgvW函数可以将完整的命令行分解为单独的token,但是此API仅支持UNICODE编码,且调用API后,需要用HeapFree来释放相关内存空间。

1.4.进程环境变量

每个进程都有一个关联的环境块(environment block)分配在进程地址空间内。

其包含的字符串大致如下:

:::\…
VarName1 = VarValue1\0
VarName2 = VarValue2\0
\0

对环境变量的操作可以通过GetEnvironmentStringsFreeEnvironmentStringsGetEnvironmentVariableExpandEnvrionmentStringsSetEnvrionmentVariable完成
Read the rest of this entry »

, , , , ,

1 Comment

LogToFile 1.1.0 Release

http://blog.kingsamchen.com/archives/546的新版

新版较之旧版改变如下:

1、在保留原有接口的基础上,增加了GetErrorMessage接口。此函数可以由GetLastError得到的错误代码获取错误文本说明,适合Win32平台上的开发

2、考虑到易用性,利用预处理解决掉了不同编译器之间的问题。即是说,在非Win32上,新增加GetErrorMessage接口不会被编译

值得注意的是,由于VC编译器的特殊性,预编译头无法用预处理解决,如果你不使用预编译,则直接删除相应预处理代码即可

3、由于接口函数DeleteFile与Win32 API发生名字重叠,所以新版函数接口改为DeleteLogFile,为了统一,相关函数也做出了修改

4、删去了仅VC支持的Error Stream处理的相关代码

新版函数接口如下

/**************************************************

** Project:LogToFile

** File:logfile.h

** Edition:v1.1.0 Demo

** Coder:KingsamChen [MDSA Group]

** Last Modify:2011-01-26

**************************************************/

#if _MSC_VER > 1000
#pragma  once
#endif

#ifndef _LOGFILE_C6A57B5F_C91D_4fc0_9B38_10524313FAE1
#define _LOGFILE_C6A57B5F_C91D_4fc0_9B38_10524313FAE1

const wchar_t ERRLOG_FILE_NAME[] = L"ErrorLog.log";

void LogErrorToFile(const wchar_t* pszFileName, wchar_t* pszFormat, ...);
void ClearLogFile(const wchar_t* pszFileName);
void DeleteLogFile(const wchar_t* pszFileName);
#ifdef _WIN32
typedef unsigned long ulong;
int GetErrorMessage(ulong ulErrorId, wchar_t* pszMessage, ulong ulBufLen);
#endif

static int WriteFile(const wchar_t* pszFileName, wchar_t* pszStr);

#endif

Download LogToFile_SRC

, , , , ,

6 Comments

Algorithms Exercise 1

回家前在图书馆碰到了刘汝佳的《算法竞赛入门经典》,于是便借回来随手翻翻

个人实在不太喜欢这类风格的书,但是觉得某些题目不错,就随手做做,权当练习

1、位数

描述:输入一个不超过10^9的正整数(由输入者负责保证),输出它的位数。例如12735的位数是5.不要使用任何数学函数,只使用四则运算。

思路1:设接受的的n位整数为N,让i从1取到9(事实上,只要不发生溢出,可以取到上限),并令 k = N / 10^i,当k == 0时,则所求位数为i。算法复杂度为O(n)

int QueryDigit(int nNum)
{
    int nDigit, nDiv;

    for (nDigit = 1, nDiv = 10; nNum / nDiv; ++nDigit)
    {
        nDiv *= 10;
    }

    return nDigit;
}

思路2:查表法。很容易想到,每个数所在的位数区间都是确定的。比如12735在10000~99999这个区间范围内。而这个区间范围内的数都是5位。查表法需要写较多的代码,但是运行起来速度会快得多,不过只有在要查询的数字比较多的情况下才能体现。易知,复杂度为O(1)

int QueryDigit(int nNum)
{
    int DigitTable[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int nDigit;

    // a long long if statement
    if (0 <= nNum && nNum < 10)
    {
        nDigit = DigitTable[0];
    }
    else if (10 <= nNum && nNum < 100)
    {
        nDigit = DigitTable[1];
    }
    else if (100 <= nNum && nNum < 1000)
    {
        nDigit = DigitTable[2];
    }
    else if (1000 <= nNum && nNum < 10000)
    {
        nDigit = DigitTable[3];
    }
    else if (10000 <= nNum && nNum < 100000)
    {
        nDigit = DigitTable[4];
    }
    else if (100000 <= nNum && nNum < 1000000)
    {
        nDigit = DigitTable[5];
    }
    else if (1000000 <= nNum && nNum < 10000000)
    {
        nDigit = DigitTable[6];
    }
    else if (10000000 <= nNum && nNum < 100000000)
    {
        nDigit = DigitTable[7];
    }
    else
    {
        nDigit = DigitTable[8];
    }

    return nDigit;
}

2、韩信点兵

描述:相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只要掠一眼排尾的人数就知道总人数。输入3个非负整数a,b,c,表示每种队形排尾人数(a<3,b<5,c<7),输出总人数的最小值。

思路1:题目实质是求x=3*k1+a = 5*k2 + b = 7*k3 + c。可以通过令x从1开始进行尝试,第一个符合的数就是所求的数。分析可知,算法的复杂度是O(n),这里的n只与所求数有关。

但事实上,存在复杂度为O(1)的算法。因为此题可以通过公式计算得到。

思路2:这道题在数论上颇有名气,其本质是求线性同余方程组的解。解法最早由宋朝秦九韶提出,并被国外数学家冠以“中国剩余定理”

ps:这本号称入门级的算竞赛辅导书(似乎还是面向中学生)居然会出现线性同余方程组求解的问题,表示这让我很诧异。

关于“中国剩余定理”的详细说明和分析将在未来几天内(起码得等我到家)完成。

由“中国剩余定理”可以得到韩信点兵问题的通解公式

知道了公式,代码就显得极其简单
Read the rest of this entry »

, , , ,

9 Comments

散列表

散列表(Hash Table)又称哈希表,是一种仅支持少数几种字典操作的表。其支持的操作有:Insert、Delete和Find

使用散列表的原因在于,对散列表中元素进行查找的期望效率很高,通常复杂度在O(1)

一、直接寻址表

在介绍散列表前,我们先介绍一种更高效的表:直接寻址表(Direct Addressing Table,DAT)

DAT查找的高效性在于采用的直接寻址技术(Direct Addressing),可以看作散列表的前身

设存在一些取自全域U的关键字元素k,即

此时n是一个不大的数,且规定

可以建立一个直接寻址表T[0...n-1](可利用数组实现)。若关键字k在表中,则必有T[k]=k,否则T[k]=NIL

易知,DAT的各种操作效率很高,时间复杂度可以达到O(1),但是局限性比较大

 

二、散列表

对于DAT来说,如果关键字全域U很大,那么实现一个大小为|U|的DAT就不太现实,且如果用到的关键字域|K|<<|U|,就会造成巨大的空间浪费

而利用散列表(Hash Table,HT),则可以保持查找期望复杂度为O(1)的情况下,是空间复杂度降低至O(|K|)

在DAT中,关键字k被存在T[k]中,而在HT中,关键字k保存在T[h[k]]中。其中,h(k)被称为散列函数(hash function),h(k)称为散列值(hash code)

散列函数h(k)利用关键字k,计算出k在HT中的位置,进而存储k

在HT中,有时会发生不同的关键字k得到相同的散列值的情况,即

这一情况称为碰撞(collision),后文对碰撞和解决碰撞的方法进行分析

 

三、散列函数

事实上,散列函数的好坏直接决定了散列表的性能,因为好的散列函数可以很大程度上降低碰撞发生的概率

在理想情况下,一个好的散列函数应满足每个关键字元素k映射到表的n个位置中是等可能发生的,且关键字之间各自独立,这个称为一致散列假设

但在实际情况中,这个假设很难成立,我们只能构造某些近似满足的情况

(1)求模取余散列法

使用求模取余散列法可能首先需要利用准备函数p(k),将关键字转化为自然数k

通过对关键字k进行模m操作来计算散列值(m为散列表大小),表达式为

此处h(k)应注意对m值的选取,由众多科学家及数学家的分析推导,m应取与2的整数次幂不太接近的一个素数
Read the rest of this entry »

, , , , ,

1 Comment