结构体与类的字节对齐(终极方案,简单易懂)

先记住常用类型在32和64位的字节

类型 32位 64位
char 1 1
int 4 4
short 2 2
float 4 4
double 8 8
指针 4 8

只有指针在64位时不同,是8。函数指针的typedef声明不参与计算。枚举类型占内存4字节。
另外注意:gcc中没有要求结构体大小是最大对象的整数倍。

字节对齐

终于搞清楚结构体的字节对齐怎么计算了,看了那么多国内博客,大部分都不靠谱,要不然就是不知所云,最后看了一个印度三哥的视频讲解,没用3分钟就明白了。其实就一条规则:计算时按最大成员的大小进行逐个判断,有需要就补位

直接看几个例子:

1
2
3
4
5
6
typedef struct bb
{
int i; //4
double w; //8
float h; //4
};

我们以为它在内存中是这样的: iiii wwwwwwww hhhh 其实是这样的:iiii ---- wwwwwwww hhhh ----
最大的double占8个,从左向右,每8个为一组,编译器无法把iiii wwww一起处理,那样就把double截断了,所以给int补4位。同理float补4位。总共24.

1
2
3
4
5
6
struct s1{
char c; //1
int i; // 4
short f; // 2
double v; // 8
};

原本是这样:c iiii ff vvvvvvvv,从左向右按8补齐,应当是:c--- iiii ff------ vvvvvvvv。c和i总共5,给c补3位就行。f没法和v组合,只能补6位,总共24.

1
2
3
4
5
6
struct s1{
short f; //2
char c[3]; //3
int i; //4
double v; //8
};

原来是:ff ccc iiii vvvvvvvv,2+3不足8,2+3+4超过了8,所以给f补3位,然后i补4位,也就是2+3+3+4+4+8=24

最后来个特殊的,计算N的大小:

1
2
3
4
5
6
7
8
9
struct Node{
char c;
int i;
char p;
};
struct N{
Node n; //12
int x; //4
};

按上面的方法可知Node占12,那么按上面的方法,N是不是该占24?错了,在N里的Node应该按cccc iiii pppp处理,这样N就占16.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct s1{
char c; //1
int i; // 4
short f; // 2
double v; // 8
};

struct s2{
char c; //1
short f; // 2
int i; // 4
double v; // 8
};

第一个的大小是24,第二个是16

#pragma pack (n)

这条预处理命令也好理解了,只要把上面规则中最大变量的大小换成n就行,注意假设结构体中最大元素占内存m,当n如果超过m是不起作用的。

类的sizeof计算

  1. 类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑。
  2. 普通成员函数和构造函数不影响sizeof的计算
  3. 虚函数由于虚指针的存在,所以要占据一个指针大小,也就是4字节,无论多少个虚函数
  4. 类的总大小也遵守字节对齐规则。

将类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
Base();
explicit Base(int a);
virtual ~Base();
void test();
virtual void test_virtual();
private:
void foo();
protected:
int m;

执行sizeof的结果是8,如果把虚析构函数去掉就变成4,再把int m去掉就变成了1,实际是空类,但是空类实例化也要在内存占用地址,由编译器添加一个字节以区分不同对象。


回调函数

之前对回调函数理解一直不到位,需要深入分析一下。
一般函数都是系统提供或程序员自定义的,让程序员使用的。但回调函数恰恰相反,它是程序员定义(注册),在特定条件(常常是用户触发)发生时由系统API调用的,是通过函数指针实现调用的。函数定义在高层,调用在底层。

Linux信号处理机制就是利用回调函数实现的,例如signaction某个形参就有一个成员是函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
void func(int n)
{
printf("signal %d catched !\n",n);
}
int main()
{
struct sigaction act;
act.sa_handler = func;
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT,&act,NULL);
}

当程序运行后,按Ctrl+C会发送SIGINT信号,然后内核调用函数func,输出文本。

类的成员函数做回调函数

由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败。定义类成员函数时,在该函数前加CALLBACK即可将其定义为回调函数。

  1. 类的静态成员函数实现回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void func(int sig)
{
cout << endl;
cout << "signal: "<<sig<<endl;
if(sig == 15)
cout << "SIGTERM"<<endl;
else
cout << "SIGINT"<<endl;
exit(0);
}

class Foo
{
public:
static void func(int sig)
{
cout << endl;
cout << "signal: "<<sig<<endl;
if(sig == 15)
cout << "receive signal SIGTERM"<<endl;

exit(0);
}
};

int main()
{
signal(SIGINT, func);
signal(SIGTERM, Foo::func); // 第二个参数也可以是func
while(1) // 设法阻塞
sleep(1);
return 0;
}

从终端启动程序,会阻塞。按下Ctrl+C,会触发SIGINT信号。执行pkill命令,会触发SIGTERM信号。


this指针

this指针指向对象的地址,本身是一个常量指针MyClass *const this,也就是不能改变指向的对象。

this指针是在创建对象前就有了,在编译时刻已经确定,this指针放在栈上。当一个对象创建后,整个程序运行期间只有一个this指针.

我是这样理解类和this指针的:类相当于房子的户型,根据这个户型可以造出很多房子,这就相当于对象。每个房子的地址不同,这就相当于内存地址。当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。this就是房子里面的一个标识,说明了房子的地址,但这个标识又不占房子空间。

this指针不属于对象本身的一部分,不会影响sizeof作用。 顺便一提,一个空的类,sizeof的大小是1

this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。
在类的赋值运算符重载函数中,我们可以一般使用*this作为当前对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base
{
public:
Base();
void test();
void test_pub();
}
//相当于void Base::test(Base* const this)
void Base::test()
{
std::cout<<this<<endl;
cout<<"base test"<<endl;
}
void Base::test_pub()
{
std::cout<<this<<endl;
cout<<"base test_pub"<<endl;
}

调用两函数的结果:

1
2
3
4
00DDF9A0
base test
00DDF9A0
base test_pub

也就是说运行时只有一个this指针。


类的静态变量和静态函数

如果有N个同类的对象,那么每一个对象都分别有自己的成员变量,不同对象的成员变量各自有值,互不相干。但是有时我们希望有某一个或几个成员变量为所有对象共有,这样可以实现数据共享。

可以使用全局变量,但用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶然失误,因此在实际开发中很少使用全局变量。

静态变量

  • 对于类的静态成员变量,只有static const int类型和 enum 类型能在类里面初始化,其他只能在.cpp里初始化,但不能在类体内初始化,因为静态数据成员为类的各个对象共享,否则每次创建一个类的对象则静态数据成员都要被重新初始化。

  • 静态成员函数和静态变量在类体外初始化时,无须加static关键字,否则是错误的。

  • static成员变量的内存空间是在初始化时分配,程序结束才释放,跟类对象的销毁无关。

  • 静态变量不参与sizeof计算,因为它不占用对象的内存

  • 静态成员仍然遵循public,private,protected访问准则。

静态函数

  • 静态成员函数只能调用静态变量,因为没有this指针。 设计它的初衷是把类名当成namespace用,控制类内的static变量。

  • 静态成员函数仍然遵循访问等级,但最好是public,否则不能直接用类名::调用

  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员。

  • 静态成员函数不能声明为虚函数,编译直接报错

  • 静态成员函数与成员函数不能同名同参数,也就是静态和非静态函数不能重载,否则编译器不知调用哪个

举例:

1
2
3
4
5
6
7
8
9
10
11
12
// 头文件
class Obj
{
private:
Obj() {}
static Obj* instance;

public:
static Obj* getInstance();
static const int n = 12;
static int n;
};

1
2
3
4
5
6
7
8
9
10
11
//源文件
Obj* Obj::instance=0; //类外定义
int Obj::n = 4; //类外定义
Obj* Obj::getInstance()
{
if(!instance)
{
instance = new Obj();
}
return instance;
}

参考:
Essential C++ 115页


单例模式的多种形态

应用实例:线程池,日志类,windows系统的任务管理器

一个单例模式应具备以下特征:

  1. 只能实例化同一个对象
  2. 可以全局访问
  3. 禁止拷贝
  4. 线程安全

针对第一条,可以将构造函数权限设为private,如果是public那么每次实例化调用构造函数,对象的内存地址都不同,也就是说只准调用一次构造函数。那问题来了,构造函数都private了,怎么实例化对象?显然要用某个public方法来调用,这又有问题:都没实例化对象,怎么调用public方法?所以这个public方法只能是静态的了。

针对第二条,全局性很容易想到静态函数,它是属于类的,而不是属于某个对象的。

第三条很容易,将拷贝构造函数和复制运算符声明为private即可。

综上,单例类的雏形应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton
{
public:
// 单例方法

private: //构造函数或析构函数为私有函数,所以该类是无法被继承的,
Singleton(){std::cout<<"单例构造"<<endl; }
~Singleton(){std::cout<<"单例析构"<<endl; }
Singleton(const Singleton &);
Singleton& operator=(const Singleton&);
static Singleton* m;
};

关键就是怎么实现public static方法。

测试1

首先想到这样的方法:

1
2
3
4
5
static Singleton instance()
{
Singleton s;
return s;
}

结果报错构造函数和析构函数是private,第一行就编译不过

测试2

测试下面这种方法

1
2
3
4
5
6
7
8
9
static Singleton* instance1()
{
Singleton *s = new Singleton();
return s;
}
Singleton* s1 = Singleton::instance1();
Singleton *s2 = Singleton::instance1();
delete s1;
delete s2;

结果发现有两个构造函数,而且无法析构。

有缺陷的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton
{
public:
static Singleton* LazyInstance()
{
if(!m)
m = new Singleton();
return m;
}
~Singleton(){std::cout<<"单例析构"<<endl; }

private:
Singleton(){std::cout<<"单例构造"<<endl; }
Singleton(const Singleton &)=delete;
Singleton& operator=(const Singleton&)=delete;

static Singleton* m;
};

测试:

1
2
3
4
5
6
7
// 在类外要初始化静态变量: SingleTon* SingleTon::instance = nullptr;
Singleton* s = Singleton::LazyInstance();
std::cout<<s<<endl;
Singleton* s2 = s;
std::cout<<s2<<endl;
delete s;
// delete s2; 不能这样,否则二次析构

s和s2的内存地址相同,说明是单例。但类中只负责new出对象,却没有负责delete对象,结果发现只调用一次构造函数,还需要手动delete s。可以用智能指针修正

如果有两个线程,假设pthread_1刚判断完 intance 为NULL 为真,准备创建实例的时候,切换到了pthread_2, 此时pthread_2也判断intance为NULL为真,创建了一个实例,再切回pthread_1的时候继续创建一个实例返回,那么此时就不再满足单例模式的要求

双检锁+智能指针的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SingleTon
{
public:
typedef boost::unique_ptr<SingleTon> Ptr;
static Ptr getInstance()
{
if(instance==nullptr)
{
std::lock_guard<std::mutex> lk(m_mutex);
if(instance==nullptr)
instance = boost::shared_unique<SingleTon>(new SingleTon);
}
return instance;
}
~SingleTon(){ qDebug()<<"destruct"; }
private:
SingleTon(){ qDebug()<<"construct"; }
SingleTon(const SingleTon& s){}
SingleTon& operator=(const SingleTon& s){}

static std::mutex m_mutex;
static Ptr instance;
};

改用智能指针做静态类型,

1
2
3
4
5
6
// 在类外要初始化静态变量: SingleTon::Ptr SingleTon::instance = nullptr;
SingleTon::Ptr s = SingleTon::getInstance();
qDebug() << s.get();

SingleTon::Ptr s1 = SingleTon::getInstance();
qDebug() << s1.get();

结果二者地址相同,也运行了析构函数。 加了锁,使用互斥量来达到线程安全。这里使用了两个if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用getInstance的方法都加锁,锁的开销毕竟还是有点大的。

缺点:使用智能指针会要求用户也得使用智能指针;使用锁也有开销; 同时代码量也增多了;在某些平台,双检锁会失效

饿汉模式

优点:不需要加锁,执行效率高,线程安全的

缺点:初始化即实例化,浪费内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singelton
{
private:
Singelton(){}
static Singelton *single;
public:
static Singelton *GetSingelton();
};
// 饿汉模式的关键:初始化即实例化
Singelton *Singelton::single = new Singelton;

Singelton *Singelton::GetSingelton()
{
// 不再需要进行实例化
//if(single == nullptr){
// single = new Singelton;
//}
return single;
}

Meyers模式

1
2
3
4
5
6
#define App SingleTon::MeyersInstance()
static SingleTon& MeyersInstance()
{
static SingleTon s;
return s;
}

这种方式所用到的特性是在C++11标准中的Magic Static特性。如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。

在 MeyersInstance() 函数内定义局部静态变量的好处是,构造函数只会在第一次调用MeyersInstance() 时被初始化, 保证了成员变量和 Singleton 本身的初始化顺序。
它还有一个潜在的安全措施, MeyersInstance() 返回的是对局部静态变量的引用, 如果返回的是指针, MeyersInstance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁。

Qt中的全局指针

Qt里有一个全局指针qApp,在任意地方都能使用,看看是不是单例模式。

QApplication中:

1
#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))

QCoreapplication中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//头文件中
#define qApp QCoreApplication::instance()

static QCoreApplication *instance() { return self; }

static QCoreApplication *self;

//源文件中
QCoreApplication *QCoreApplication::self = 0;
// 构造函数
QCoreApplication::QCoreApplication()
{
d_func()->q_ptr = this;
d_func()->init();
QCoreApplicationPrivate::eventDispatcher->startingUp();
}

// 就是 d_func()->init();
void QCoreApplicationPrivate::init()
{
......
Q_ASSERT_X(!QCoreApplication::self, "QCoreApplication", "there should be only one application object");
QCoreApplication::self = q;
......
}

从QCoreapplication来看,qApp是个宏,实际是函数QCoreApplication::instance(),QCoreApplication这个类十分关键,构造函数肯定不能是private。从这个self来看,特别像懒汉模式,self是在QCoreApplication构造函数里赋值,赋给它的q指针实际就是QCoreApplication的this指针。

但是在程序里使用qApp,你会发现其地址都一样,也就是同一个全局指针,这就在于Q_ASSERT_X这句限定了只能有一个QCoreApplication对象,再加上拷贝构造函数和赋值运算符都在QObject限定为private,因此qApp也是一种单例模式。所以我们可以说单例模式不一定限定构造和析构是private,这个使用了Qt特有的d指针和q指针,技巧性太高,还是用meyers模式吧

ROS中的单例模式

看ROS源码中的类TopicManager,它用到了单例模式,我模仿写了一个类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SingleTon;
typedef boost::shared_ptr<SingleTon> SingleTonPtr;

#define Ptr SingleTon::getInstance()

class SingleTon
{
public:
SingleTon() {}
static const SingleTonPtr& getInstance()
{
static SingleTonPtr f = boost::make_shared<SingleTon>();
return f;
}
void out() { cout<<" out put "<<endl; }
private:
int m_num;
};

结果发现构造函数只能是public,如果是private,就会报错,原因在make_shared中。这样一来就不能实现单例了,看来这种做法不可行。


参考:探究 C++ Singleton(单例模式)


五大cast转型

const_cast

  • const_cast是唯一能将const性质转化掉的操作符,但执行其他任何类型的转换都会引起编译错误。

单纯的把常量转为变量是没多少意义的,也就是这种代码:

1
2
const int con =12;
int cc= static_cast<int>(con);

直接用变量或者mutable就好了。

如果定义了一个非const的变量,却使用了一个指向const值的指针来指向它(不规范的风格),在程序的某处我们想改变这个变量的值了,但手头只持有指针,这是const_cast就可以用到了:

1
2
3
4
int constant = 26;
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 3;

这并不是一个好的设计,还是应该遵从这样的原则:使用const_cast去除const限定的目的绝对不是为了修改它的内容,只是出于无奈,所以最好少用转型。

如果有一个函数,它的形参是non-const类型变量,而且函数不会对实参的值进行改动,这时我们可以使用类型为const的变量来调用函数,也就用到const_cast了。

1
2
3
4
5
6
7
8
9
void InputInt(int * num)
{
cout<<*num<<endl;
}
int main()
{
const int constant = 21;
InputInt(const_cast<int*>(&constant));
}

dynamic_cast

dynamic_cast用于类继承层次间的 指针或引用 的转换。和qobject_cast类似,用于执行安全的向下转型

dynamic_cast 只能用于含有虚函数的类,虚函数表是是dynamic_cast操作符转换能够进行的前提条件。虚表的最前面是指向 type_info 的指针,包含了类的继承信息、类的描述等。能否转换成功,所依据的就是type_info如果转换失败,new_type 是指针,那么将返回NULL;如果 new_type 是引用,那么将抛出异常 std::bad_cast

向下转型有两种情况。一种是基类指针指向派生类的对象,这种转换是安全的;另一种是基类指针所指对象为基类类型,在这种情况下dynamic_cast在运行时做类型检查,转换失败,返回结果为0

最常用的情况是这样:

1
2
3
Base* bb = new Derive();
Derive* dd = dynamic_cast<Derive*>(bb);
dd->test();

如果没有虚函数,会报错: error: C2683: “dynamic_cast”:“Base”不是多态类型

dynamic_cast在基类和派生类指针之间转换时,会遍历整个继承体系进行类型检查,因此dynamic_cast时间和空间代价较高。比如4级的继承结构,dynamic_cast<Base>将会调用4次才能确定最终的那个子类型,尤其避免连续使用转型,用虚函数代替这种情况。

static_cast

类似于C风格的强制转换。无条件转换,静态类型转换。用于:

  • 子类转换为父类:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的,此时用dynamic_cast

  • 基本数据类型转换,例如double, int, char, float等,不能进行无关类型(如非基类和子类)指针之间的转换。

  • void* 转换为某类型指针

1
2
double a = 1.999;
int b = static_cast<double>(a); //相当于a = b ;

大多数的编译器都会给出一个警告:从“double”转换到“int”,可能丢失数据。static_cast可以明确告诉编译器,这种损失精度的转换是在知情的情况下进行的

1
2
3
4
double a = 1.999;
void * vptr = & a;
double * dptr = static_cast<double*>(vptr);
cout<< *dptr <<endl; //输出 1.999

从这里能看出,static_cast进行的是简单粗暴的转换,static_cast不做运行时的类型检查以保证转换的安全性,所以static_cast不如dynamic_cast安全,其正确性完全由程序员自己保证。

reinterpret_cast

十分危险,平时不要使用。

  • 从底层对数据进行重新解释,依赖具体的平台,可移植性差

  • 可以在指针和引用随意的转换

  • 可以将整型转换为指针,也可以把指针转换为数组

qobject_cast

参考:qobject_cast 分析及QTBUG 20616


静态绑定和动态绑定

对于非虚成员函数,C++是静态绑定的,而虚函数都是动态绑定,如此才可实现多态性。这也是C++语言和其它语言Java, Python的一个显著区别。

几个名词定义:

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,表现一个对象将会有什么行为,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

看这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base
{
public:
Base();
virtual ~Base();
void test();
void test_pub();
virtual void test_virtual();
private:
int m;
};

void Base::test()
{
cout<<"base test"<<endl;
}
void Base::test_pub()
{
m = 10;
cout<<"base test_pub"<<endl;
}
void Base::test_virtual()
{
cout<<"base virtual"<<endl;
}

调用:

1
2
3
4
Base *b = NULL;
b->test();
b->test_pub();
b->test_virtual();

如果对三个函数分别测试,会发现只有test()能正常运行,后两个函数运行时会等待一小段时间,然后就返回了。

指针为空,仍然能正常运行test函数,这是因为代码进行了静态绑定,在编译期就确定了。那为什么test_pub不行?因为涉及到了this指针,也就是说这两个函数实际是这样的:

1
2
3
4
5
6
7
8
9
void Base::test(Base* this)
{
cout<<"base test"<<endl;
}
void Base::test_pub(Base* this)
{
this->m = 10;
cout<<"base test_pub"<<endl;
}

如果this指针是NULL,当然无法操作成员变量,所以报错。在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址(即this指针)作为一个隐含参数传递给函数。说白了,test函数传入了一个错误的参数,但又没用到这个参数,所以可以运行。但如果是静态函数,那么编译器不会给它传this指针。

这里对成员函数的解析,和查找对应的代码都是在编译阶段完成的,这就是所谓的静态绑定。只要函数不涉及this指针,空类指针也可以调用函数,C++只关心指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性,这也是为什么规范的代码经常会有if(b!=NULL)这样的语句。

那么test_virtual为什么也不能正常运行?test_virtual是虚函数,对它的调用只能等到运行期才能确定,然后才发现是空指针,无法运行。

参考:C++中的静态绑定和动态绑定


strcat strcpy strlen strcmp

strcat

我自己写的是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char* _strcat(char *_Dest, const char *_Source)
{
assert((_Dest != NULL) && (_Source != NULL));
char* p= _Dest;
while(*p!='\0')
{
p++;
}
const char* temp = _Source;
while( *temp!='\0')
{
*p=*temp;
p++;
temp++;
}
return _Dest;
}

strcat的源码如下:
1
2
3
4
5
6
7
8
9
10
char *strcat(char *des, const char *src)
{
assert((des!=NULL) && (src!=NULL));
char* address = des;
while(*des != '\0') // 移动到字符串末尾
++des;
while(*des++ = *src++) //dest的下一个字符用src的下一个字符赋值
;
return address;
}

必须加assert判断,src指向的字符添加到dest所指向的结尾处,覆盖原来的\0,添加新的\0。二者所指的内存区不可重叠,并且dest要有足够的内存容纳src字符串,但源码也没有对此判断,所以它是危险的。注意其中dest不能指向常量区。

这样用是错的,p1指向了常量字符串:

1
2
3
char *p1 = "123";
char *p2 = "ABC";
std::cout<<strcat(p1,p2)<<endl;

改变连接的起始点:

1
2
3
char p1[4]="123";
char p2[4]="ABC";
std::cout<<strcat(p1+2,p2)<<endl;

结果是3ABC

以’\0’作为连接的标志:

1
2
3
char p1[40]="123\0abc";
char p2[8]="ABC\0DEF";
std::cout<<strcat(p1,p2)<<endl;

结果是123ABC

strcpy

函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
char* _strcpy(char *dst, const char *src)
{
assert((dst!=NULL)&&(src!=NULL));
char* p1 =dst;
const char* p2 = src;
while(*p1!='\0')
{
*p1 = *p2;
p1++;
p2++;
}
return dst;
}

函数不安全,src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
源指针所指的字符串内容是不能修改的,因此应该声明为 const 类型。与strcat相比,没有先移动到dst末尾。注意复制src到\0为止。

测试代码:

1
2
3
char s1[]="12345";
char s2[]="abc";
std::cout<<strcpy(s1,s2)<<endl; //abc

复制s2到s1时,以\0为准。

strlen

我写的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
int _strlen(const char* s)
{
assert(s!=NULL);
char* p =s;
int n=0;
while(*p!='\0')
{
p++;
n++;
}
return n;
}

注意指针不能为NULL,不统计最后的’\0’

strcmp

两个字符串的大小不能用关系运算符判断,应该用strcmp(a,b),两个字符串相同时返回0,a>b返回一个正值,否则返回负值.
从左向右逐个比较a,b中的字符,根据二者的ASCII值确定结果,如果二者一直相同,但字符串a长度较小,那么结果为负。
我用的编译器对正负的处理是1和-1,有的是ASCII的差值。
我是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int _strcmp(const char *dst, const char *src)
{
assert((dst!=NULL)&&(src!=NULL));
const char* p1 = dst;
const char* p2 = src;
while(*p1!='\0' && *p2!='\0')
{
if(*p1 > *p2)
return 1;
else if(*p1 < *p2)
return -1;
p1++;
p2++;
}
if(*p1!='\0' && *p2=='\0')
return 1;
else if(*p1=='\0' && *p2!='\0')
return -1;
return 0;
}

一种源码是这样的:

1
2
3
4
5
6
7
8
9
int strcmp(const char *s1, const char *s2)  
{
//两字符串相等的情况,如果两个字符串中所有的字符相等,则返回0
while (*s1 == *s2++) //两个字符相等
if (*s1++ == 0) //达到字符串末尾
return (0);
//不相等,则返回值为第一次不相等时的两字符之差
return (*(unsigned char *)s1 - *(unsigned char *)--s2);
}

以上几个函数中的src全要加const限定,strcmp的两个参数都要加const

参考:


深入探究虚函数 (一)

在实现C++多态时会用到虚函数。使用虚函数目的是通过基类指针访问派生类定义的函数。

典型使用:

1
2
Base* ba = new Derive();
Derive *de = ba;

反射性地会说ba是个基类指针,指向派生类对象。但是运行程序竟然报错了: error: C2440: “初始化”: 无法从Base * 转换为 Derive * 从基类型到派生类型的强制转换需要 dynamic_cast 或 static_cast 。也就是需要向下转型,为什么?ba不是指向派生类对象吗?

这就是虚函数的动态绑定, 在运行期才指向派生类对象,而在编译期还是基类指针,不能赋给de,语法检查就过不了,当然报错了。

我们一般说ba的静态类型是Base*,动态类型是Derive*。如果第一行改为Base* ba,那么静态类型是Base*,没有动态类型。

  • 所谓静态多态是指通过模板或者函数重载实现的多态,其在编译期确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。

  • 普通函数在编译时就确定了函数地址,但虚函数在运行时查询虚函数表,所以效率较低。

  • 要正确实现虚函数,只能通过基类的指针或引用来指向派生类对象

  • 派生类重新实现基类中的虚函数,这就叫覆盖

  • 成员虚函数不能有同名同参数的静态函数

  • 多个虚函数对sizeof的计算相当于一个,因为它们都存在虚函数表里,虚函数表只对应一个虚指针,所以不会扩大占用内存

纯虚函数

基类有纯虚函数时,就是抽象类,抽象类无法实例化,也无法声明指针。派生类可以不实现这个纯虚函数,但这样就没有意义了。 基类中的纯虚函数其实也可以实现,但没有必要,因为派生类都会重新实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
virtual void testPureVirtual() = 0;	 // 基类中声明纯虚函数

void Base::testPureVirtual() // 基类中实现纯虚函数
{
cout<<"base pure virtual test "<<endl;
}

void Derived::testPureVirtual() // 派生类中实现虚函数
{
Base::testPureVirtual();
cout<<"derived pure virtual test"<<endl;
}
Derived d;
d.testPureVirtual();

这里的代码在派生类中调用了基类中的纯虚函数,由于抽象类无法实例化,所以基类纯虚函数是不能直接调用的。

不要重新定义继承而来的虚函数的默认参数值

绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
Base();
virtual ~Base();
virtual void test_virtual(int i=0); // 默认值为0
};
void Base::test_virtual(int i)
{
cout<<"base virtual "<<i<<endl;
}

class Derive : public Base
{
public:
Derive();
void test_virtual(int i=1); // 默认值改为1
};
void Derive::test_virtual(int i)
{
cout<<"derive virtual "<<i<<endl;
}

还是那个常用的调用:

1
2
Base* ba = new Derive();
ba->test_virtual();

结果竟然是:
1
derive virtual  0   // 函数为派生类的,默认值为基类的

原因在于虚函数是动态绑定的,但参数是静态绑定的, 覆盖虚函数时只能覆盖动态绑定的部分,所以不能改变参数。虚函数的效率较差,如果对参数也进行动态绑定,那么对执行速度和编译器简易度不利,所以C++做了分开的处理。

不能声明为虚函数的函数

构造函数

首先明确一点,在编译期,编译器完成了虚表的创建,而虚指针在构造函数期间被初始化

如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。

內联函数

编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。 二者不是同一范畴的东西。

静态成员函数

静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。也就是说,静态成员函数与类有关,而虚函数与类的实例对象有关。 二者也不是同一范畴的东西。

多态

多态存在的3个必要条件:

  1. 继承
  2. 函数的重写
  3. 父类引用或指针指向子类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B0    //基类BO声明
{
public: //外部接口
virtual void display() //虚成员函数
{
cout << "B0 display0" << endl;
}
};
class B1 :public B0
{
public:
void display() { cout << "B1 display0" << endl; }
};

// 尝试形参改为 B0& ptr 或 B0* ptr
void fun(B0 ptr) //普通函数
{
ptr.display();
}
B0 b0; //声明基类对象
B1 b1; //声明派生类对象
fun(b0); //调用基类B0函数成员
fun(b1); //调用派生类B1函数成员

运行结果是B0 display0 B0 display0
虚函数的动态绑定仅在 基类指针或引用 绑定派生类对象时发生,fun的形参不是指针而是对象,所以调用哪个版本的函数在编译时就已经确定。

参考:
C++虚函数表剖析
深入C++对象模型&虚函数表


C++中protected访问权限问题

今天发现有这样两句话:

  1. 基类的保护成员对于派生类的成员是可访问的。
  2. 派生类的成员只能通过派生类对象访问基类的保护成员,派生类对一个基类对象中的受保护成员没有访问权限。

这两句话看的太头晕了,其实作者应该是想表达:只有在派生类体内,才可以通过派生类对象,访问基类的protected成员。看这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
Base();
private:
void foo();
protected:
int m;
};

class Derive : public Base
{
public:
Derive();
};
Derive::Derive()
{
this->m = 1; // 这里访问m没有问题
}

使用类的代码:

1
2
Derive *d = new Derive();
std::cout<< d->m <<endl; // 这里就报错了

总结一下三个关键字的访问范围:

  • private: 只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行。
  • protected: 只能被类成员函数、子类函数及友元访问,不能被其他任何访问,本身的类对象也不行 也就是说protected比private多了个子类函数
  • public: 以上所说全能访问

还是上面的两个类,只看public和private,如下代码:

1
2
3
4
Base *b = new Base();
b->test_pub(); // 通过
b->foo(); // 报错
std::cout<< b->m; // 报错

protected继承和private继承能降低访问权限:

  • 使用private继承,父类的protected和public属性在子类中变为private

  • 使用protected继承,父类的protected和public属性在子类中变为protected

  • 使用public继承,父类中的protected和public属性不发生改变