-
static关键字:
因为静态是属于类的,它是不知道你创建了10个还是100个对象,所以它对你对象的函数或者数据是一无所知的,所以它没办法调用,而反过来,你创建的对象是对类一清二楚的(不然你怎么从它那里实例化呢),所以你是可以调用类函数和类成员的.
- static修饰局部变量,可以让该局部变量拥有全局变量的特性,修饰函数则可以让该函数只是在本文件可见,也就是说不用担心该函数名或者变量名与同项目中其他文件的重名而编译失败。所以一般不让外部使用的函数都加static关键字修饰。
- 修饰类中的成员数据,会是它们变成该类中共享的,而不再是每个对象都拥有自己的一份。
- 修饰类中的成员函数,静态函数可以被对象调用,也可以通过类调用。
- 静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态.
-
mutex和自解锁:
在C++11中,引入了有关线程的一系列库.且都在std命名空间内.下面演示一个使用线程的例子.非常的简单.引入了thread和mutex头文件.
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int tmp = 0;
mutex g_mutex;
void c()
{
for (int i = 0; i < 20; ++i)
{
g_mutex.lock();
cout << this_thread::get_id() << " " <<tmp++ << endl;
g_mutex.unlock();
}
}
int main()
{
thread t1(c);
thread t2(c);
if (t1.joinable())
t1.join();
if (t2.joinable())
t2.join();
system("pause");
return 0;
}
上面是使用互斥锁,每次都要自己去加锁解锁,有更好的方法,那就是使用自解锁 (lock_guard),不用再手动解锁。如下代码
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int tmp = 0;
mutex g_mutex;
void c()
{
for (int i = 0; i < 20; ++i)
{
lock_guard<mutex> lock(g_mutex);
cout << std::this_thread::get_id() << " " << tmp++ << endl;
}
}
int main()
{
thread t1(c);
thread t2(c);
if (t1.joinable())
t1.join();
if (t2.joinable())
t2.join();
system("pause");
return 0;
}
当g_mutex锁进行初始化,初始化的时候就是锁的lock操作的时候,那么什么时候unlock呢?就是在超出它的作用域之后析构时unlock.
-
枚举:
枚举常量与宏的区别主要有几点:
- 枚举常量是实体中的一种,但宏不是实体;
- 枚举常量属于常量,但宏不是常量;
- 枚举常量具有类型,但宏没有类型,枚举变量具有与普通变量相同的诸如作用域、值等性质,但宏没有,宏不是语言的一部分,它是一种预处理替换符。枚举类型主要用于限制性输入,例如,某个函数的某参数只接受某种类型中的有限个数值,除此之外的其它数值都不接受,这时候枚举能很好地解决这个问题。能用枚举尽量用枚举,否则在调试的时候你是看不到当时的值的。
- 用宏去定义一个变量如果你定义了一个相同的变量那么要看谁在前面,如果宏在前面变量会产生编译错误,而且这个错误很难查找,如果那个宏隐藏的很深的话。如果你定义的变量在前那么更可怕了,直接没有错误,但是宏定义被自定义的变量悄悄替换了。用枚举定义的话不管你定义的顺序前后关系怎样都会产生重复定义的错误。从上面的举例来看枚举比宏好用的多。宏还有一个特性是没有作用域,这里的作用域是指宏定义以后的代码都可以使用这个宏。宏可以被重复定义这个可能导致宏的值被修改。所以建议不要用宏去定义整形的变量,用枚举或者const。又会有用const还是枚举呢,世界一向如此纠结,枚举只能表示整形,const可以修饰任何类型。整形的情况下如果你要定义几种有关系的数值那么用枚举,否则用const。
用enum关键字说明常量有以下几点好处:
(1) 使程序更容易维护
enum Error_Code
{
OUT_OF_MEMORY = 20,
INSUFFICIENT_DISK_SPACE = 30,
LOGIC_ERROR = 40,
FILE+NOT_FOUND =100,
};
同样,你也可以用#define指令说明类似的一组常量,请看下例:
#define OUT_OF_MEMORY 20
#define INSUFFICIENT_DISK_SPACE 30
#define LOGIC_ERROR 40
#define FILE_NOT_FOUND 100
上述两例的结果是相同的。
(2) 使程序调试起来更方便
枚举与宏定义的区别和联系:
- 枚举和define都可以swtich,枚举是类型安全的,define不是类型安全的。枚举只能定义整形值,define能定义几乎任何值。
- 宏和枚举之间的差别主要在作用的时期和存储的形式不同,宏是在预处理的阶段进行替换工作的,它替换代码段的文本,程序运行的过程中宏已不存在了。而枚举是在程序运行之后才起作用的,枚举常量存储在数据段的静态存储区里。宏占用代码段的空间,而枚举除了占用空间,还消耗CPU资源。
- 但也不能就此说宏比枚举好,如果需要定义非常多的常量,用一条enum {.....}明显比一大堆define更清晰,枚举也可以用来定义一些特殊类型,比如Bool,如: type enum {FALSE,TRUE} Bool;
define特点:
优点:宏定义可为多种类型的值,如字符串、整型、浮点型等。
缺点:没有范围限制(全局范围有效),容易发生冲突,产生不确定的结果;
多个相关的值一起定义时比较散乱。
enum特点:
缺点:只能为整型值
优点:遵循范围规则,不易和其它定义发生冲突。
多个相关值一组,比较清晰。
一般情况下二者可选时尽量用enum。
枚举和CONST常量:
写程序时,我们常常需要为某个对象关联一组可选alternative属性.例如,学生的成绩分A,B,C,D等,天气分sunny, cloudy, rainy等等。
更常见的,打开一个文件可能有三种状态:input, output和append. 典型做法是,对应定义3个常数,即:
const int input = 1;
const int output = 2;
const int append = 3;
然后,调用以下函数:
bool open_file(string file_name, int open_mode);
比如,
open_file("Phenix_and_the_Crane", append);
这种做法比较简单,但存在许多缺点,主要的一点就是无法限制传递给open_file函数的第2个参数的取值范围,只要传递int类型的值都是合法的。(当然,这样的情况下的应对措施就是在open_file函数内部判断第二个参数的取值,只有在1,2,3范围内才处理。)
使用枚举能在一定程度上减轻这种尴尬(注1),它不但能实现类似于之前定义三个常量的功能,还能够将这三个值组合起来成为独一无二的组。例如:
enum open_modes {input = 1, output, append};
以上定义了open_modes为枚举类型enumeration type。每一个命名了的枚举都是唯一的类型,是一个类型标示器type specifier。例如,我们可以重新写一个open_file函数:
bool open_file(string file_name, open_modes om);
在open_modes枚举中,input, output, append称为枚举子enumerator, 它们限定了open_modes定义的对象的取值范围。这个时候,调用open_file函数和之前的方法还是一模一样:
open_file("Phenix_and_the_Crane", append);
但是,如果传递给open_file的第二个参数不是open_modes枚举类型值的话(注1),那么编译器就会识别出错误;就算该参数取值等价于input, output, append中的某个,也一样会出错哦!例如:
open_file("Phenix_and_the_Crane", 1);
例如:
//使用const:
const int MON =1;
const int TUE=2;
const int WED=3;
const int THU=4;
const int FRI=5;
const int SAT=6;
const int SUN=7;
//使用#define//定义一个整型变量,为整型变量赋以下值
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
//使用枚举//定义一个枚举变量,此变量可以具有多个可能的值,
typedef enum weekDay{
MON=1, //枚举类型中数据都是整型且从0开始,此处将第1个值设为1,则TUE , // 以下均从1开始递加
TUE,
WED, //C++中逗号不是一条语句,不是一条语句就可以用回车分行
THU, //这样有助于写注释
FRI,
SAT=7, //可以再重新赋值,此时SAT=7,而不是6
SUN //SUN=8,而不是7
}week_day;
week_day week=SUN;
hashtable和hashmap的区别
1、HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2、Hashtable比HashMap多提供了elments() 和contains() 两个方法。
3、HashMap键值都可以为NULL,而Hashtable只支持key-value一种(即key和value都不为null这种形式)。
既然HashMap支持带有null的形式,那么在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断,因为使用get的时候,当返回null时,你无法判断到底是不存在这个key,还是这个key就是null,还是key存在但value是null。
4、线程安全性不同,HashMap的方法都没有使用synchronized关键字修饰,都是非线程安全的,而Hashtable的方法几乎都是被synchronized关键字修饰的,是线程安全的。
但是,当我们需要HashMap是线程安全的时,怎么办呢?我们可以通过Collections.synchronizedMap(hashMap)来进行处理,亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5、初始容量大小和每次扩充容量大小的不同
Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。
HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
6、计算hash值的方法不同
为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。
Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。
Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高,为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2 的幂次方带来的效率提升给抵消掉。
hashmap的数据结构是数组+链表的形式
也就是用拉链法来处理hash冲突的。
hashmap加入自定义类型
#include <hash_map>
#include <string>
#include <iostream>
using namespace std;
//define the class
class ClassA{
public:
ClassA(int a):c_a(a){}
int getvalue()const { return c_a;}
void setvalue(int a){c_a;}
private:
int c_a;
};
//1 define the hash function
struct hash_A{
size_t operator()(const class ClassA & A)const{
// return hash<int>(classA.getvalue());
return A.getvalue();
}
};
//2 define the equal function
struct equal_A{
bool operator()(const class ClassA & a1, const class ClassA & a2)const{
return a1.getvalue() == a2.getvalue();
}
};
int main()
{
hash_map<ClassA, string, hash_A, equal_A> hmap;//当时用自定义类型时,需要自己传入后俩参数,平时则是默认的
ClassA a1(12);
hmap[a1]="I am 12";
ClassA a2(198877);
hmap[a2]="I am 198877";
cout<<hmap[a1]<<endl;
cout<<hmap[a2]<<endl;
return 0;
}
I am 12
I am 198877
宏函数的优缺点及使用
我们来看一个例子,比较两个数或者表达式大小,首先我们把它写成宏定义:
#define MAX( a, b) ( (a) > (b) (a) : (b) )
其次,把它用函数来实现:
int max( int a, int b)
{
return (a > b a : b)
}
很显然,我们不会选择用函数来完成这个任务,原因有两个:
首先,函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;
其次,函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。
宏函数的缺点:
每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
宏函数使用的经典例子:
有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。
#define MALLOC(n, type) \
( (type *) malloc((n)* sizeof(type)))
int *ptr;
ptr = MALLOC ( 5, int );
//将这宏展开以后的结果:
ptr = (int ) malloc ( (5) sizeof(int) );
利用这个宏,我们就可以为任何类型分配一段我们指定的空间大小,并返回指向这段空间的指针。
这个例子是宏定义的经典应用之一,完成了函数不能完成的功能.
但是宏定义也不能滥用,通常,如果相同的代码需要出现在程序的几个地方,更好的方法是把它实现为一个函数。
define的多行定义
//宏定义写出swap(x,y)交换函数
/#define swap(x, y)\
x = x + y;\
y = x - y;\
x = x - y;
关键是每一个换行的后面要加 " \ "
C++拷贝构造函数和赋值运算符重载
拷贝构造函数:就是用一个已经有了的对象去初始化刚生成的对象,也是在这个时候会调用该函数。
它的重点在于用已经存在的初始化刚生成的。
提到拷贝构造,就不得不提及浅拷贝和深拷贝,主要是在对象是指针的时候,因为当对象为指针时,在进行简单的赋值,只是将两个指针指向了同一块内存,这样一块区域的修改就会影响另一个,所有在有指针的时候,就不能使用系统自己生成的拷贝构造函数了。
像这样两个指针指向同一块内存的就是浅拷贝。
如果不自己重写拷贝构造函数,那么在调用时就会报错,因为会对同一块内存析构两次。
要自己实现,在函数内部也要自己申请一块内存,这样就可以避免上述问题了。下面是重写后的拷贝构造函数:
class Rect
{
public:
Rect() // 构造函数,p指向堆中分配的一空间
{
p = new int(100);
}
Rect(const Rect& r)
{
width = r.width;
height = r.height;
p = new int; // 为新对象重新动态分配空间
*p = *(r.p);
}
~Rect() // 析构函数,释放动态分配的空间
{
if(p != NULL)
{
delete p;
}
}
private:
int width;
int height;
int *p; // 指针成员
};
当类里有成员是指针时,并且动态分配了内存,我们应该重载赋值函数,实现一个深拷贝(physical copy)。
此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。
赋值运算符重载:就是对 “=”进行的重载,让它可以实现对象的直接赋值。
如果有指针的话,就需要重写该函数,一般需要重写拷贝构造函数的,同时也要重写赋值运算符重载函数。c++代码如下:
class Rect
{
public:
Rect() // 构造函数,p指向堆中分配的一空间
{
p = new int(100);
}
Rect(int a, int b, int *ppth) :width(a), height(b), p(ppth) {}
Rect(const Rect& r)
{
width = r.width;
height = r.height;
p = new int; // 为新对象重新动态分配空间
*p = *(r.p);
}
Rect& operator=(const Rect &r)
{
if (this != &r) //防止自赋值
{
if (p != NULL)
delete p;
width = r.width;
height = r.height;
p = new int;
*p = *(r.p);
}
return *this;
}
~Rect() // 析构函数,释放动态分配的空间
{
if (p != NULL)
{
delete p;
}
}
public:
int width;
int height;
int *p; // 指针成员
};
int main()
{
int *p1 = new int(50);
int *p2 = new int(550);
Rect a1(10, 20, p1);
Rect a2(a1);
cout << a1.height << " " << a1.width << " " << *(a1.p) << endl;
cout << a2.height << " " << a2.width << " " << *(a2.p) << endl;
Rect a3;
cout << a3.height << " " << a3.width << " " << *(a3.p) << endl;
a3 = a1;
cout << a3.height << " " << a3.width << " " << *(a3.p) << endl;
return 0;
}
当类里有成员是指针时,并且动态分配了内存,我们应该重载赋值函数,实现一个自己的拷贝构造函数和赋值运算符的重载函数。
重写,重载和隐藏的区别
重载:就是函数名相同,参数和类型不同的函数就构成重载,在调用函数时,编译器会根据参数的传递来判断要调用那个函数。要构成重载必须要在同一个作用域下,且函数名字相同参数列表不同。
重写:就是父类函数加virtual关键字,子类只对其函数内容做改变,而不改变函数名和参数列表。这样就构成重写,也就是C++的运行时期的多态。
重载可以理解为静态的多态,重写则是动态的多态。
隐藏:只要父类函数未加virtual关键字,子类的函数名又与父类一样,这样就算子类函数隐藏了父类函数,隐藏也就可以理解为是对父类函数的屏蔽。
代码举例:
#include <iostream.h>
using namespace std;
class Base
{
public:
virtual void f(float x)
{ cout << "Base::f(float) " << endl; }
void g(float x)
{ cout << "Base::g(float) " << endl; }
void h(float x)
{ cout << "Base::h(float) " << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x)
{ cout << "Derived::f(float) " << endl; }
void g(int x)
{ cout << "Derived::g(int) " << endl; }
void h(float x)
{ cout << "Derived::h(float) " << endl; }
};
(1)函数Derived::f(float)重写了Base::f(float)。
(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。
类的声明与包含头文件的区别
类的前置声明:一般用在.h文件中,这样的声明就只是可以使用类指针或者引用,不可以生成该类的对象,自然也就不可以调用该类的方法了。
包含头文件:一般用在.cpp文件中,因为这个文件是函数具体实现的地方,需要调用类的成员方法,所以必须要包含那个类的头文件,这样在调用类成员方法的时候编译器才不会报错。
但是具体怎么用还是要究其根本:就是看是否需要使用类方法,还是仅仅有个声明,只是用一下那个类的指针或者引用
举例如下:
// ------------A.h
#include "B.h"
class A
{
B b;
public:
A(void);
virtual ~A(void);
};
//-----------A.cpp
#include "A.h"
A::A(void)
{
}
A::~A(void)
{
}
// ------------B.h
#include "A.h"
class B
{
A a;
public:
B(void);
~B(void);
};
// ------------B.cpp
#include "B.h"
B::B(void)
{
}
B::~B(void)
{
}
好的,完成,编译一下A.cpp,不通过。再编译B.cpp,还是不通过。编译器都被搞晕了,编译器去编译A.h,发现包含了B.h,就去编译B.h。编译B.h的时候发现包含了A.h,但是A.h已经编译过了(其实没有编译完成,可能编译器做了记录,就将A.h视为编译过了,这样可以避免陷入死循环。编译出错总比死循环要强点),就没有再次编译A.h就继续编译。后面发现用到了A的定义,这下好了,A的定义并没有编译完成,所以找不到A的定义,就编译出错了。提示信息如下:
1>d:/vs2010/test/test/a.h(5): error C2146: syntax error : missing ';' before identifier 'b'
1>d:/vs2010/test/test/a.h(5): error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
1>d:/vs2010/test/test/a.h(5): error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
而要解决这个问题就需要用到类的前置声明,如下:
//A.h
class B;
class A
{
B* b;
public:
A(void);
virtual ~A(void);
};
//A.cpp
#include "B.h"
#include "A.h"
A::A(void)
{
b = new B;
}
A::~A(void){...}
B.h
class A;
class B
{
A a;
public:
B(void);
~B(void);
};
B.cpp
#include "A.h"
#include "B.h"
B::B(void)
{
a = New A;
}
B::~B(void){...}
前置声明在友元函数中的应用
如果在一个类A的声明中将另一个类B的成员函数声明为友元函数F,那么类A必须事先知道类B的定义;类B的成员函数F声明如果使用类A作为形参,那么也必须知道类A的定义,那么两个类就互相依赖了。要解决这个问题必须使用类的前置声明。例如:
House.h
#include "Bed.h"
class CHouse
{
friend void CBed::Sleep(CHouse&);
public:
CHouse(void);
virtual ~CHouse(void);
void GoToBed();
void RemoveBed(){}
};
/House.cpp
#include "House.h"
CHouse::CHouse(void){...}
CHouse::~CHouse(void)
{int i = 1;}
void CHouse::GoToBed(){...}
///Bed.h
class CHouse;
class CBed
{
int* num;
public:
CBed(void);
~CBed(void);
void Sleep(CHouse&);
};
//Bed.cpp
#include "House.h"
CBed::CBed(void)
{
num = new int(1);
}
CBed::~CBed(void){delete num;}
void CBed::Sleep(CHouse& h){...}
#pragma once和#ifndef的区别
为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别。
方式一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
... ... // 声明、定义语句
#endif
方式二:
#pragma once
... ... // 声明、定义语句
#ifndef的方式受C/C++语言标准支持。它不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。
缺点是:如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,编译器却硬说找不到声明。
由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长。
#pragma once一般由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。
好处是:你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。大型项目的编译速度也因此提高了一些。
缺点是:如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。
#pragma pack的理解与使用
1.#pragma简述:
在所有的预处理指令中,#Pragma
指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma
指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。
2.#pragma pack的作用:
#pragma pack
的主要作用就是改变编译器的内存对齐方式,这个指令在网络报文的处理中有着重要的作用。
#pragma pack(n)
是他最基本的用法,其作用是改变编译器的对齐方式, 不使用这条指令的情况下,编译器默认采取
#pragma pack(8)
也就是8字节的默认对齐方式,n值可以取(1
, 2
, 4
, 8
, 16
) 中任意一值。
3.#pragma pack详细介绍:
#include <iostream>
using namespace std;
#pragma pack(show)
#pragma pack(4)
#pragma pack(show)
#pragma pack(push,16)
#pragma pack(show)
#pragma pack(pop)
#pragma pack(show)
int main()
{
return 0;
}
输出结果如下:
对该结果进行分析