目录
1.前向声明
前向声明就是不用引入某个头文件,就可以使用这个头文件中声明的一些数据结构,这样做一方面可以加快编译速度,最重要的是可以解决头文件相互引用的情况。
先讲述一下头文件相互引用的危害:
假设A.h中引用了头文件B.h,B.h中引用了头文件A.h,那么如果有一个源文件中includeA或者B头文件,那么在预编译时,是需要把头文件的内容复制到源文件中去的,这样就会形成无限复制,复制A.h时又需要复制B.h,然后B.h中又有A.h,这样循环往复。
但是有些情况又必须互相引用,这样就可以使用前向声明,就不用在A.h中引用B.h,只是把B.h中需要用到的数据结构在A中做一个声明,告诉编译器编译时不要报错,然后在源文件中引用时A.h和B.h一起include,这样A.h和B.h的内容就一起复制到源文件中,这样A.h中的声明就有了定义。
以一个小例子为例:
互相包括,会无限复制
/*A.h*/
#include "B.h"
struct A
{
public:
int a = 10;
struct B *b;
};
/*B.h*/
#include "A.h"
struct B
{
char b;
};
struct A *a;
前向声明:
/*A.h*/
#include "B.h"
//这个头文件可加可不加,但是如果这里加了头文件,在源文件中要是想include A.h和B.h时
//只需要引用A.h就够了,如果这里不加,那就在源文件中都要引用A.h和B.h。
struct A
{
public:
int a = 10;
struct B *b;
};
/*B.h*/
//B.h中就没有这个引用了。
struct A;//这里只是一个不完全类型的声明,就是只知道A是一个类,但不知道A这个类具体内容,
//需要占多少内存,所以在B.h中只能定义struct A的指针或者引用类型(引用类型详解见下方)
struct B
{
char b;
};
struct A *a;
2.未命名的命名空间
一般命名空间的定义和声明是这样的:
/*命名空间声明,一般是在头文件中*/
namespace test
{
int a;
int func(int a);
}
/*命名空间中内容的定义,一般是在源文件中*/
using namespace test;
int func(int a)
{
//具体内容省略
}
/*或者这样的:*/
int test::func(int a)
{
//具体内容省略
}
/*或者这样:*/
namespace test
{
int test::func(int a)
{
//具体内容省略
}
}
在muduo源码中会出现这样的情况:
namespace
{
int a;
int func(int a);
};
这种被称之为未命名的命名空间,其作用是该命名空间中的内容的作用域只是当前的源文件,而不能被其他源文件使用,比如上述的命名空间定义是在A.cpp中,那么里面的函数func和变量就不能在B.cpp中使用。
注意的一点是未命名的命名空间没有声明,直接进行定义,所以只在源文件中会出现,不会在头文件中出现,而命名的命名空间应该在头文件中定义。
3.全局作用域符号::
在muduo库中经常看到一些函数或者变量在使用时,前面会加上::(双冒号),其作用是表示该函数或者变量是全局函数或者变量。例子如下:
char zhou; //全局变量
void sleep()
{
char zhou; //局部变量
zhou(局部变量) = zhou(局部变量) *zhou(局部变量) ;
::zhou(全局变量) =::zhou(全局变量) *zhou(局部变量);
}
注意:全局函数是指在类外面声明的函数,加不加extern都可以,因为extern是函数默认的声明。
4.“=default” 、“=delete”
=default使用
一个类的创建会默认生成一些默认函数,具体如下:
(1)构造函数
(2)析构函数
(3)拷贝构造函数
(4)拷贝赋值函数(operator=)
(5)移动构造函数
以及全局的默认操作符函数
(1)operator,
(2)operator &
(3)operator &&
(4)operator *
(5)operator->
(6)operator->*
(7) operator new
(8) operator delete
主要的函数具体举例如下:
class MyClass
{
public:
MyClass(const char * str = nullptr); // 默认带参构造函数 // 默认构造函数指不带参数或者所有参数都有缺省值的构造函数
~MyClass(void); // 默认析构函数
MyClass(const MyClass &); // 默认拷贝构造函数
MyClass & operator =(const MyClass &); // 默认重载赋值运算符函数
}
但是如果在类中实现了这些函数,那么就不会提供这些默认函数了,以构造函数为例,如果我们想在自定义了构造函数之外,还想要默认的构造函数,就可以使构造函数=default,这样就相当于默认函数也起作用了
举例说明:
/*test.h*/
class test
{
public:
test(int a, int b);
test() = default;//1
int add();
void seta(int a);
void setb(int a);
std::tuple<int, int> show();
private:
int a;
int b;
};
/*test.cpp*/
test::test(int a, int b)
{
test::a = a;
test::b = b;
}
int test::add()
{
return test::a + test::b;
}
void test::seta(int a)
{
test::a = a;
}
void test::setb(int b)
{
test::b = b;
}
std::tuple<int, int> test::show()
{
std::tuple<int, int> a(test::a, test::b);
return a;
}
如果没有1处的声明,直接定义一个test变量,如下:
test a;
编译器会报错,而有了1处的声明,这样定义就不会报错了。
所以1处的作用就是加上类的默认构造函数定义。其实也有一种办法可以自定义一个无参数的构造函数,
比如以上的例子可以加一个这样的函数
test(){};
但是和用=default有区别,这样就不再是一个POD类型了,可能让编译器失去对这样数据类型的优化功能。(有待进一步理解)
=delete使用
就是可以删除上面讲的那些默认函数,使得类中没有这一项默认函数
举例说明:
test(const test&) = delete
删除test类的拷贝构造函数
可以参考这篇文章:https://blog.csdn.net/a1875566250/article/details/40406883
5.static_cast使用
其实与static_cast类似的有四种关键词,都是强制类型转换,统一的格式是:
cast-name<type>(experession)
其中type是转换的目标类型,而experession是要转换的值。
cast-name有以下四种关键词:(这里就写两种了)
static_cast:具有明确定义的类型转换(只要不包含底层const)
其中void*和其他类型指针之间的转换使用static_cast来操作
const_cast:将底层const对象转换成其他类型
具体可以参考这篇博客
https://www.cnblogs.com/chenyangchun/p/6795923.html
6.顶层const和底层const
个人理解:顶层const是当const修饰的是指针指向的值或者变量时,这个const是顶层const,
底层const是当const修饰指针本身,也就是指针常量,这个const是底层const。
注意:顶层底层都是用来修饰const的,而不是修饰变量的,所以一个指针既可以是顶层const,也可以是底层const
具体例子见C++primer。
7.模板类的静态变量
可以参考这篇文章:https://blog.csdn.net/itcastcpp/article/details/37812623
特别注意的是一种变量类型就可以定义一种变量类型的静态变量
8.for_each()函数
for_each()函数的具体内容如下:就是把迭代器从first到last位置依次作为fn的入口参数来执行fn
Function for_each(InputIterator first, InputIterator last, Function fn)
{
while (first!=last) {
fn (*first);//first指针指向的值作为fn函数的入口参数
++first;
}
return fn; // or, since C++11: return move(fn);
}
9.类中的运算符重载(主要针对加减乘除这种算数运算符)
算数运算符重载可以在类内,也可以在类外,大部分都是在类外的,并且可以使用友元来操作类的私有变量
使用友元的程序例子:
class A
{
public:
explicit A(int d1, int d2):a(d1),b(d2){}
friend int operator+(A t1, A t2);//声明友元函数,就是为了让函数能够访问类的私有变量
private:
int a;
int b;
};
int operator+(A t1, A t2)//友元函数不需要前面加类的作用于A::
{
return t2.a+t2.b+t1.a+t1.b;
}
int main()
{
A t1(1,2);
A t2(3,4);
cout<<t1+t2<<endl;
}
不适用友元的程序例子:
class A
{
public:
explicit A(int d1, int d2):a(d1),b(d2){}
int a;
int b;//只能把变量变成共有的,这样类外函数才能访问
};
int operator+(A t1, A t2)
{
return t2.a+t2.b+t1.a+t1.b;
}
int main()
{
A t1(1,2);
A t2(3,4);
cout<<t1+t2<<endl;
}
为什么算数运算符不定义在类内
举例说明
class A
{
public:
explicit A(int d1, int d2):a(d1),b(d2){}
/*情况一*/
int operator+(A t1, A t2)
{
return t2.a+t2.b+t1.a+t1.b;
}
/*情况二*/
int operator+(A t1)
{
return t1.a+t1.b;
}
private:
int a;
int b;
};
int main()
{
A t1(1,2);
A t2(3,4);
cout<<t1+t2<<endl;
}
情况一不可行,情况二可以,但是这种定义的类型只能是一个类变量进行操作,用处不是很大。(有待完善)
10.类的临时对象
如果直接类名+初始化值,那么会创建出一个临时变量,并且执行构造函数,并且在这个代码行结束时,执行析构函数。
程序举例如下:
class test
{
public:
test(int a, int b):t1(a),t2(b){ std::cout<<"start"<<std::endl;}
void add()
{
std::cout<<t1+t2<<std::endl;
}
~test(){std::cout<<"byebye\n";}
private:
int t1;
int t2;
};
int main()
{
test(3, 5).add();//这里没有定义类变量,所以是临时创建一个类,在这个一行代码结束后就会自动销毁
return 0;
}
结果如下,可以看到执行了构造函数,所要求执行的析构函数,以及最后的析构函数。
11.纯虚函数
在muduo库中经常看到这样的声明:
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels) = 0;
在声明后面加等于0,并不表示返回值等于0,而是表示这个函数是一个纯虚函数,留待派生类中去定义,所以在这个类中是无法被调用的
12.父类指针指向子类
父类指针可以指向子类,但是子类指针不能指向父类,具体原理可以参考下面的博客
https://blog.csdn.net/jiangqin115/article/details/78292937
并且注意的是父类指针指向子类指针以后,能够使用的子类成员只有子类从父类中继承过来的成员,而不能使用子类独有的成员。
并且不是所有的父类指针都可以指向子类的,共有继承的都可以,具体原理可以参考下面的博客:
https://blog.csdn.net/liufei_learning/article/details/21587085
13.柔性数组
在muduo中,看到了有关柔性数组的用法,比如以下的情况就是柔性数组:
struct test
{
int len;
int arr[];
}
其中arr就是一个柔性数组,之所以叫柔性,是因为这个数组所占内存可大可小。
首先如果创建一个普通的栈上的变量a
struct test a;
这个a所占内存为4个字节,也就是一个int类型变量占的内存大小。也就是说arr数组没有内存空间
测试如下:
struct test
{
int len;
int arr[];
};
int main()
{
struct test a;
cout<<sizeof(a);
(void)a;//之所以加这个,是因为避免局部变量未使用的警告
system("pause");
}
原理就是int arr[]不占用内存,这里有两点需要明确:1.数组名和指针用法上类似,但是本质上不一样,数组名其实和变量名一样,在编译的过程中,会被替换成对应值的存储地址,但是指针在编译时,会被替换成存储对应值地址的地址。
有点绕,用下图来说明,假设还有一个指针ptr指向arr
int *ptr = arr;
画一个更加形象的图片
所以在编译时,arr自动被替换为2948344,而ptr被替换为3472248。这就是指针和数组本质的区别。
2.就是变量名是不占用内存空间的,只是变量名锁指向的值会占用内存。
至于具体赋值,可以采用创建动态内存的方法来扩展柔性数组如下:
struct test
{
int len;
int arr[];
};
int main()
{
struct test *a = (test*)malloc(sizeof(test)+sizeof(int)*5);
a->arr[4] = 10;
cout << a->arr[4];
free(a);
system("pause");
}
更详细一点的可以查看这个博客https://www.cnblogs.com/veis/p/7073076.html
其中注意这个博客中所说的那个内存错误,每个数组的-1位置和size位置都是标志位。
注意:这种柔性数组在struct中使用是没有问题的,但是在class中使用时,就不一定能保证有用了。其中最主要的原因是不是你数组放在最后一个,你就真的在存储位置上也是结构体末尾的位置了,因为class里面特性较多,而且存储方式复杂,不是结构体那样简单顺序存储。
比如如果protected data放在private data后面就不行,具体可以参考《深度探索C++对象模型》的20页。
13.非类型形参
参考这篇博客:https://www.cnblogs.com/gw811/archive/2012/10/25/2738929.html
14.左值右值和引用
左值和右值的解释(个人理解)
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
右值当在赋值号右边取出值赋给其他变量的值;右值在CPU寄存器中存储。
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址
值存在内存中肯定是持久。存在寄存器中肯定是短暂的,这就是左值和右值的区别。
下面这个例子中:
int a=10;//a是左值
int b=20;//b是左值
b=a;//b是左值,a变量的值是右值
个人理解是:a和b都是左值,但是把a的值赋给b时,a的值先从内存中拿出来,再放到b的内存中去,所以a的值会被存储在寄存器上,所以a的值就是右值。所以左值是可以转换成右值的。
左值引用和右值引用
int t=10;
int &a=t;//a是左值引用
int &&b=10;//b是右值引用
左值引用的等式右边必须是左值,就是有确定内存的
特例:
const int &a=10;//这个是可以的,按理10是右值,但是可以绑定在a上,这是规定
右值引用的等式右边必须是右值,就是临时变量,注意这种情况下,左值就不能转换成右值了,也就是如下情况是错误的
int a=10;
int &&b=a;//错误
右值引用的好处:就是所引用的右值对象没有其他用户,可以自由接管所引用的对象资源,个人理解就是把临时变量或者常量赋值给一个变量,也就是这样的程序是等价的
int &&a=10;
int b=10;//个人理解是等价的
引用的本质
int a=10;
int &b=a;
int *c;
c=&a;
注意,这里的b和指针是有差别的,指针c中存放的是a变量的地址,但是引用类型b只是一个a的别名,最有说服力的证明就是:
&a,&b,&c三个操作中,前面两个的值是一样的,但是&c的值不一样,所以就可以当成b和a是一个东西,无论是变量地址还是变量值,而c和a不是同一个东西,最主要的是变量地址不一样。
但是从底层来看,肯定b变量的地址和a变量的地址是不一样的,b变量存储的是a变量的地址,但是在&b时,编译器会自动翻译成&(*b)。具体可以看
https://blog.csdn.net/lws123253/article/details/80353197
还有一种说法就是引用只是在编译的符号表中多一条符号对应,但并不会在内存中为引用变量真的开辟存储空间,可以参考博客https://www.cnblogs.com/qingergege/p/6723509.html(这种说法是错误的)
引用和指针其实都是占据内存空间的,只是引用在编译器中会经过处理,所以表现和指针有所不一样。
使用std::move函数可以将左值强制转换成右值,其实就是移动值
15.function和bind的理解
暂时只理解function对于函数和bind对象的调用。从这个角度来看:
对于function,我的理解是一个智能函数指针,用一个小例子来说明更清楚:
#include <iostream>
#include "functional"
extern test b;
using namespace std;
typedef function<void()> null_func;//null_func所定义的函数指针形如:void (*function)()
typedef function<void(int)> int_func;//int_func所定义的函数形如:void (*function)(int)
typedef function<void(int, const char*)> int_char_func;//int_char_func所定义的函数形如:void (*function)(int, const char*)
void show(int a, const char* b)//但我想把show函数的指针赋给上面三个定义的函数指针,就需要用到bind
{
cout << "数值为:" << a << endl;
cout << "字符串为:" << b << endl;
}
int main()
{
int_char_func test1 = show;//因为show函数的形式和int_char_func定义的形式一致,所以直接赋值即可
test1(1, "test1");
int_func test2 = bind(show, placeholders::_1, "test2");//show比int_func所定义的函数形式多了一个入口参数,所以就需要把这个入口参数绑定到一个具体的值上,然后只留下一个入口参数,也就是int,留下的入口参数用占位符placeholders::_1来表示
test2(2);
null_func test3 = bind(show, 3, "test3");//show比null_func所定义的函数形式多了两个入口参数,所以在bind绑定的时候,需要把两个入口参数都和具体的对象绑定在一起。
test3();
int_char_func test4 = bind(show, placeholders::_1, placeholders::_2);//其实给int_char_func定义的对象赋值还有一种方法,可以使用bind,但是由于int_char_func定义的函数有两个入口参数,和show一致,所以bind中使用两个占位符
test4(4, "test4");
system("pause");
}
结果如图所示
其实相比于普通的函数指针,通过function和bind,可以实现只要定义一种函数指针,所有形式的函数都可以将作为这个函数指针的值。
注意:占位符一定有要有命名空间std::placeholders::_1
注意bind如果绑定的是类成员变量时,会和之前的绑定有所不一样。
using namespace std;
typedef function<void()> null_func;
typedef function<void(int)> int_func;
typedef function<void(int, char*)> int_char_func;
class bindfunc
{
public:
bindfunc(int t) :c(t) {};
void show(int a, const char* b)
{
cout << "数值为:" << a << endl;
cout << "字符串为:" << b << endl;
cout << "私有变量为:" << c << endl;
}
private:
int c;
};
int main()
{
bindfunc a(10);
int_char_func test1 = bind(&bindfunc::show, &a, placeholders::_1, placeholders::_2);
//有两点需要注意的地方,1.这里需要传入的第一个参数需要带成员函数所属类
//2.紧接着需要的参数是这个成员函数所属哪一个类对象的指针(智能指针也可以)
test1(1, "test1");
system("pause");
}
注意:指针中普通指针和智能指针shared_ptr都可以,测试程序如下:
#include <iostream>
#include <unistd.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <memory>
using namespace std;
typedef function<void()> null_func;
typedef function<void(int)> int_func;
typedef function<void(int, char*)> int_char_func;
class bindfunc
{
public:
bindfunc(int t) :c(t) {};
void show(int a, const char* b)
{
cout << "数值为:" << a << endl;
cout << "字符串为:" << b << endl;
cout << "私有变量为:" << c << endl;
}
private:
int c;
};
int main()
{
shared_ptr<bindfunc> a(new bindfunc(10));//这里a是智能指针,但是好像只有share_ptr可以使用
int_char_func test1 = bind(&bindfunc::show, a, placeholders::_1, placeholders::_2);
test1(1, "test1");
}
注意:unique_ptr就不可以,weak_ptr肯定也不可以,所以我认为只有shared_ptr可以