一、C++初步认识
1、const
C语言中常使用#define预处理命令来定义符号常量,如#define PI 3.14 。它只是在程序预编译的时候进行字符置换,PI不是变量,没有数据类型,不占用存储单元,而且容易出错。
C++提供了用const定义常变量的方法,如const float PI = 3.14; 其中PI有数据类型,占用存储单元,只不过在程序运行期间它的值不能改变。需要注意的是const 定义常变量时必须进行初始化。
在程序中定义的全局变量,一般在其他文件中也能访问,只需在其他文件中声明一下该全局变量即可,如extern int count;。而对于const全局变量如果要使其他文件能访问该变量,则在变量定义的时候也要加上extern,如extern const int count = 0;。
2、函数的声明
在对函数进行声明时,可以采用简化形式,如:
int max(int x, int y);
int max(int, int);
int max();
max();
3、函数的重载
函数的重载指在同一作用域中用同一个函数名定义多个函数,其参数类型或参数个数不同,eg:
void TestFunc(int n)
{
}
void TestFunc(float f)
{
}
而如果参数类型相同,仅仅一个是变量,一个是常量的话,不能构成重载。比如下面的两个函数的参数,一个是int型变量,一个是int型常量,不能构成函数的重载,编译会出错:
void TestFunc(int n)
{
}
void TestFunc(const int n)
{
}
类似的,下面的两个函数的参数,一个是字符串指针变量,一个是字符串指针常量,同样不能构成函数的重载:
void TestFunc(char* p)
{
}
void TestFunc(char*const p)
{
}
而下面的两个函数的参数,一个是普通字符串指针变量,一个是const字符串指针变量,所以可以构成函数的重载:
void TestFunc(char* p)
{
}
void TestFunc(const char* p)
{
}
4、函数模板
函数模板是一个通用函数,其函数类型和参数类型不具体指定,用一个虚拟的类型来表示,在调用函数时,系统会根据实参的类型来得到具体的函数。如:
template <typename T> T max(T x, T y)
{
if (x > y)
return x;
else
return y;
}
template <typename T> T min(T x, T y)
{
if (x < y)
return x;
else
return y;
}
.......
cout << max<int>(5, 6) << endl;//也可以不加<int>,让编译器自动推导类型,如 max(5, 6)
cout << min<float>(3.14, 3.15) << endl;
其中T为虚拟的类型,可以随意命名,可以有多个虚拟类型,如template <typename T1, typename T2>。typename关键字也可以用class替换。
虚拟的类型也可以为一个类eg:
template<typename T>
void TestFun(T t)
{
t.print();
}
class CObj
{
public:
void print()
{
cout << "print func" << endl;
}
};
int main()
{
CObj o;
TestFun(o);
return 0;
}
模板还可以来表示一个数值,对象等,eg:
template<int N>
void TestFunc()
{
cout << N << endl;
}
.......
TestFunc<10>(); //输出10
TestFunc<20>(); //输出20
模板方法的实现应该在头文件中,如果不在头文件中的话需要在实现文件中添加模板的具体类型声明:
/*func.h*/
template <typename T> T my_max(T x, T y);
/*func.cpp*/
template <typename T> T my_max(T x, T y)
{
if (x > y)
return x;
else
return y;
}
template int my_max(int a, int b);
template double my_max(double a, double b);
5、带默认参数的函数
void fun1(int a, int b, int c=0);
......
fun1(3, 4);
因为形参与实参是从左到右一 一对应的,所以带默认参数的形参必须在形参列表的最右端。
如果有函数声明的话应在函数的声明中给出默认值。
6、变量的引用
int a;
int& b = a;
cha* p;
char*& t = p;//正确,声明指针的引用
int&* q = pt;//错误,不能定义指向引用的指针
引用又称”别名“,它不占用额外存储空间,它就代表了原变量,与其所代表的变量占用同一空间,对变量引用的操作既是对变量的操作。
同const一样,声明引用后必须对它进行初始化,且在函数执行期间,该引用不能再表示其他变量。可以声明类的成员变量为引用变量,但必须使用初始化列表对该引用变量进行初始化。
引用的主要功能是利用它作为函数的参数,使用引用传递,避免值传递,值传递就是调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
可以声明常引用,不允许通过该引用改变代表变量的值,如:
int a = 5;
const int&b = a;
b = 3;//错误
a = 3;//正确
常引用主要用于希望形参值在函数运行期间不被改变。
还可以将“引用”作为函数返回值类型,在内存中不产生被返回值的副本,也正是因为这点原因,所以返回一个局部变量的引用是不可取的。
补充一点,经我在VS2015下测试,如下的Foo构造函数只执行了一次,这说明如果把临时对象直接传给参数的话,相当于是引用传递(至少VS2015下是这样):
class Foo
{
public:
Foo()
{
Foo* p = this; //地址为0x13fd58
int a = 0;
}
virtual ~Foo()
{
int a = 0;
}
};
void func(Foo f)
{
Foo* p = &f; //地址同样为0x13fd58
int a = 0;
}
int main()
{
func(Foo());
return 0;
}
7、内置函数
内置函数又称内联函数,内嵌函数,其在编译时将函数代码直接嵌入到主函数中。它可以提高程序运行速度,一般用于规模很小而是用频繁的函数。关键字inline用来指定内置函数,如:
inline int power(int x)
{
reutrn x*x;
}
内置函数与带参数的宏定义有些类似,但带参数的宏定义容易出错,如定义一个求平方的函数:
#define power(x) x*x
cout << power(1+1); //输出为3,而非4
8、作用域运算符
在全局变量和局部变量同名时,局部变量会屏蔽全局变量,而若想使用全局变量时则可以加上作用于运算符::,如:
float a = 3.5;
int main()
{
int a = 5;
cout << ::a; //输出为3.5
}
9、动态分配内存
C语言中主要用malloc()和free()函数来动态申请、释放内存,而C++中用new和delete,申请内存失败会返回NULL,eg:
new int; //一个int
new int(300); //指定初始值的int
new int[100]; //int型数组
delete p;//释放变量空间
delete[] p; //释放数组空间
malloc/free是函数,而new/delete是运算符,执行效率高。
new分配内存时,知道存储的数据类型,返回为具体类型的指针,而malloc只知道存储空间的大小,返回类型为void*。
如果分配的是类的对象,new会执行对象的构造函数,delete会执行对象的析构函数。
new分配内存时,还可以对其赋初值,如int* p = new int(100); MyClass*p = new mcls(1001, "wang");
二、类和对象
1、结构体与类
C++允许用struct来声明一个类类型,而实际上用struct声明的结构体也就是类。
用struct声明的类如果成员不做访问属性声明,则默认为public;用class声明的类如果成员不做访问属性声明,默认为private。
struct的默认继承方式为public,而class的默认继承为private
2、成员函数
成员函数可以在类体重定义,也可以在只在类体内声明,在类的外面定义,当在类体中定义时,成员函数自动成为inline内置函数。
需要注意的是如果要指定类体外定义的成员函数为inline内置函数则类的声明和成员函数的定义必须在同一文件内。
一个C++类对象所占用的空间取决于数据成员所占的空间,与成员函数无关,在定义一个对象时, 只为类中成员变量分配内存空间, 成员函数代码是存储在对象空间之外的,所以对象之间是共享成员函数。this指针指向本类对象,它会作为一个隐含参数传递给类的非静态成员函数,当不同对象调用同一个类的成员函数时,根据成员函数的this指针所指向的不同对象来确定应该使用哪个对象的数据成员。
一般是将类的声明放在一个头文件中,而类中成员函数的定义放在一个源文件中。在实际工作中,经常将多个功能相近的类的声明放在一个头文件中,形成类库。类库有两种:一是编译系统提供的标准类库,而是用户自定义类库。
C++类自动提供的成员函数:默认构造函数、默认析构函数、复制构造函数(拷贝构造函数)、赋值运算符的重载函数(operator=)。
3、构造函数和析构函数
构造函数用来处理对象数据成员的初始化,它没有返回值,在定义对象时自动执行,如果用户没有自定义构造函数,那么C++系统会自动隐式的提供一个不带参数的构造函数。不带参数的构造函数又称默认构造函数,即如果我们没有显示定义构造函数的话,编译器会隐式提供给我们一个默认构造函数。
一些情况下,默认构造函数必须提供,比如一个类myClass,我们提供了自定义的构造方法,这样就没有了系统提供的默认构造方法,现在有一个myClass类型的vector,如果使用vector vc = vector<myClass> v1(10); 这种方式来初始化容器的话就会报错,因为这种方式会使用默认构造方法,再比方说使用v1.resize(20)来设置容器大小的话也会报错,所以我们还应该提供一个默认构造方法。
构造函数还可以使用“参数初始化表”来对数据成员初始化,如:
class box
{
public:
box(int h=0, int w=0, int l=0);
~box();
int volume();
private:
int height;
int width;
int length;
};
box::box(int h, int w, int l):height(h), width(w), length(l){}
box::~box()
{
cout << "Destructor called" << endl;
}
int box::volume()
{
return height*width*length;
}
构造函数初始化时只能采用初始化列表的三种情况:
1.需要初始化的数据成员是类的对象,而且该数据成员的构造函数是带参数的构造函数的情况。
2.需要初始化const数据成员。
3.需要初始化reference引用数据成员。
包含以上三种任一类型的类,不再提供隐式的默认构造方法,因为必须使用初始化列表来显示定义一个构造方法。
不能用参数初始化表进行初始化的情况:数据成员是静态static类型。
一个类中可以定义多个构造函数,即构造函数的重载。在调用构造函数的时候不必给出实参的构造函数称为默认构造函数,一个类只能有一个默认构造函数。
不带参数的构造函数和全部参数都带默认值的构造函数都属于默认构造函数,故二者不能同时存在。
如果构造函数的全部参数都带默认值,则在定义对象时可以不给出实参,如:box b1;
在一个类中定义了全部带默认值的构造函数后,不能再定义重载构造函数,因为会出现歧义,如:
box(int=10, int=10, int=10);
box(int, int);
box b1(15, 20);//调用第一个构造函数还是第二个?
析构函数是对象生命周期结束时,自动调用的,用来执行清理工作的函数。
如果用户没有自定义析构函数,那么C++系统也会自动生成一个空的构造函数。
析构函数不带参数和返回值。
基类中的构造函数先于派生类的构造函数执行,派生类的析构函数先于基类的析构函数执行。
4、对象数组
Student studAry[50];//定义对象数组
Student studAry[3] = {60, 70, 78};//如果Student类的构造函数只有一个参数
Student studAry[3] = { //如果Student类的构造函数有两个参数
Student(101, 87),
Student(102, 98),
Student(103, 61)
};
5、指向对象的成员函数的指针
下面为指向普通函数的指针使用:
void (*p)(); // 定义指向void型函数的函数指针p
p = fun1; //p指向fun1函数
(*p)(); //调用fun1函数
下面为指向对象中成员函数的指针:
box b1;
void (box::*p)();
p = box::fun1();
(b1.*p)();
6、再谈const
const int a;//常变量
const int b = 0;
const int& c = b; //常引用
int b = 0;
const int& c = b; //常引用也可以指向非常变量,但不能使用该引用来改变该变量的值
c = 1; //error!
const char* p = "abc"; //指向常变量的指针,即该指针指向常变量,也可以指向非常变量(这种情况下不能通过该指针修改变量的值),但该指针可以多次指向不同的常变量
char* const p; //指向变量的常指针,该指针在初始化后就不能再指向其它变量
指向常变量的指针可以指向非常变量,但不能通过该指针改变该变量的值。
const Time t1(2014, 10, 24);//常对象
const int hour;//常数据成员
void GetTime()const;//常成员函数
如果一个对象为常对象,则只能调用该对象的常成员函数。
常成员函数中只能引用数据成员但不能改变其值,常成员函数中只能调用常成员函数。如果想要在常成员函数中修改成员变量的值,可以将成员变量声明为mutable。
如果常成员函数的声明跟定义不在一个文件的话,其定义中也要加上const后缀。
常数据成员只能通过参数初始化表来进行初始化,因为const型变量是不能用=再赋值的,只能进行初始化。
const只能用于非static的类的成员函数。
构造函数初始化时只能采用初始化列表一共有三种情况:
1.需要初始化的数据成员是对象(继承时调用基类构造函数)
2.需要初始化const数据成员
3.需要初始化引用数据成员
不能用参数初始化表进行初始化:数据成员是静态static类型。
7、对象的赋值和复制
对象的赋值:
相同类的对象之间可以用“=”来进行赋值,如:
Student stud1, stud2;//stud1和stud2对象都已存在
......
stud1 = stud2;
上面的赋值操作会调用operator=,C++类自动包含一个默认的operator=,但只是对数据成员(包括私有成员)的简单的赋值。如果成员包含特殊内容,比如动态分配的数据,则应该自己重载运算符“=”。
class CItem
{
public:
CItem(const std::string str, int n) :m_str(str), m_n(n) {}
std::string getString() { return m_str; }
int getInt() { return m_n; }
std::string m_str;
private:
int m_n;
};
CItem item1("a", 1), item2("b", 2);
item1 = item2;
std::string s = item1.getString(); // "b"
int n = item1.getInt(); // 2
vector的赋值操作也会使用成员对象的operator=来进行:
class CItem
{
public:
CItem(int n): m_n(n){}
CItem& operator=(const CItem& item)
{
if (&item == this)
return * this;
m_n = 999;
return *this;
}
int getNum() { return m_n; }
private:
int m_n = 0;
};
std::vector<CItem> vc1 = { 100 }, vc2 = { 200 };
vc1 = vc2;
int nnnn = vc1[0].getNum(); // 999
对象的复制:
可以用一个已有的对象来复制出多个相同的对象,有两种方式,如:
//box1对象已经存在,box2对象还未定义
Box box2(box1);
Box box2 = box1;
使用上面的两种方法来定义对象的时候都不会调用operator=,而是调用复制构造函数。如果一个构造函数的第一个参数是自身类类型的引用(一般都声明为本类对象的常引用),且其他参数都有默认值,那么这个函数就是复制构造函数,也成拷贝构造函数。
C++类中包含一个默认的复制构造函数,其只是简单的复制类中的每个数据成员(包括私有成员)。如果成员包含特殊内容,比如动态分配的数据,则应添加自己的复制构造函数。
class CItem
{
public:
CItem(const std::string str, int n):m_str(str), m_n(n){}
std::string getString() { return m_str; }
int getInt() { return m_n; }
std::string m_str;
private:
int m_n;
};
CItem item1("a", 1), item2("b", 2);
CItem item3 = item2;
std::string s = item3.getString(); // "b"
int n = item3.getInt(); // 2
CItem item4(item2);
s = item4.getString(); // "b"
n = item4.getInt(); // 2
对象的赋值是对一个已经存在的对象赋值,而对象的复制是从无到有地建立一个新对象。
9、对象的初始化
C++对象有两种初始化方式,如果使用的是等号来初始化一个对象就是“拷贝初始化”,如果使用的是非等号来初始化一个对象就是“直接初始化”,“直接初始化”要求编译器使用函数匹配来选择与参数最匹配的构造函数。“拷贝初始化”要求编译器将等号右侧对象拷贝到正在创建的对象中,拷贝初始化一般使用拷贝构造函数来完成,但如果一个类中有一个移动构造函数,那么拷贝初始化有时会使用移动构造函数:
std::string s1("abc"); //直接初始化,编译器选择使用构造方法string (const char* s)
std::string s2(s1); //直接初始化,编译器选择使用拷贝构造方法string (const string& str)
std::string s3(10, '0'); //直接初始化,编译器选择使用构造方法string (size_t n, char c)
std::string s4 = "abc"; //拷贝初始化,使用拷贝构造方法
std::string s5 = s1; //拷贝初始化,使用拷贝构造方法
拷贝初始化不仅在使用等号定义变量的时候使用,以下情况也会使用拷贝初始化:
①、将一个对象作为实参传递个一个非引用的形参。
②、从一个非引用类型的方法中返回一个对象。
③、用花括号初始化列表来初始化容器,容器的成员使用拷贝初始化。
④、使用容器的push、insert方法方法来插入成员,新插入的成员使用拷贝初始化。
以上情况都会调用对象的默认拷贝构造函数(如果没有定义自定义拷贝构造函数的情况下)或者自定义拷贝构造函数来进行拷贝初始化,所以需要注意的是对于使用的是默认拷贝构造函数的情况,对于对象的成员仅仅是简单的赋值(浅拷贝)。
9、静态数据成员和静态成员函数
......
static int a;//静态数据成员
static int volume();//静态成员函数
......
静态数据成员和静态成员函数在对象之外开辟存储空间,它属于类而不属于某个指定对象,在类被声明之后就可以直接使用。
静态成员函数中只能引用静态数据成员和静态成员函数,普通成员函数可以引用静态数据成员和静态成员函数。
静态数据成员不能用参数初始化表来对其初始化,而且它只能在类体外进行初始化,如下代码所示,而且只能在源文件中进行初始化,如果未对其初始化则编译器自动将int类型初始化为0。在VS中必须对静态数据成员进行声明或初始化,否则会编译出错。
静态成员函数没有this指针,所以只能访问本类中的静态数据成员。
class MyClass
{
public:
static void display_static(){ cout << num << endl;}
void display(){ cout << name << num << endl;}
private:
static int num;
string name;
};
int MyClass::num = 100;
int _tmain(int argc, _TCHAR* argv[])
{
MyClass::display_static();
return 0;
}
10、const与static
静态函数只能引用静态数据成员和静态函数。
常成员函数能引用非常数据成员但不能改变其值,而且其只能引用常函数,如果一个对象为常对象,则只能调用该对象的常成员函数。
11、友元
友元使可以访问与其有好友关系的类中的私有变量,使用关键字friend。
在声明类时,在类体中用friend对一个函数进行声明,那么这个函数就成了类的友元函数,这个函数就可以访问类的私有成员了,如
//将普通函数display()声明为Time类的友元函数,使display()中能够调用Time类对象的私有成员
class Time
{
public:
Time() { m_second = -1; }
friend void display(Time& );
private:
int m_second;
};
void display(Time& t)
{
cout << t.m_second << endl;
}
int main()
{
Time t;
display(t);//输出为-1
getchar();
return 0;
}
//将Time类的成员函数display()声明为Date类的友元函数,使display()中能够调用Date类对象的私有成员
class Date;
class Time
{
public:
void display(Date& dat);
private:
int m_second;
};
class Date
{
public:
Date() { m_day = -1; }
friend void Time::display(Date& dat);
private:
int m_day;
};
void Time::display(Date& dat)
{
cout << dat.m_day << endl;
}
int main()
{
Time tim;
Date dat;
tim.display(dat);
getchar();
return 0;
}
还可以将一个类声明为另一个类的友元类,如将B类声明为A类的友元类,那么在B类的成员函数就可以调用A类对象的私有成员。声明方法为在A类的定义中声明B类为自己的友元类:friend B; 或friend class B; 如:
class Date;
class Time
{
public:
void display(Date& dat);
private:
int m_second;
};
class Date
{
public:
Date() { m_day = -1; }
friend Time;
private:
int m_day;
};
void Time::display(Date& dat)
{
cout << dat.m_day << endl;
}
int main()
{
Time tim;
Date dat;
tim.display(dat);
getchar();
return 0;
}
使用友元的情况:
当类A的成员是私有的,而类或函数B却需要访问该成员的时候,我们可以声明B为其友元,使这个私有成员仅对B开放,而不必将这个成员声明为public。
当重载类的运算符的时候经常会使用到友元,如下使CFoo支持<操作:
class CFoo
{
private:
friend bool operator<(const CFoo& f1, const CFoo& f2);
int m_iNum = 0;
};
bool operator<(const CFoo& f1, const CFoo& f2)
{
return f1.m_iNum < f2.m_iNum;
}
当然,一般我们想要对象支持<比较的话,可以直接在对象所属的类中实现operator<,而不是像上面那样使用外部方法来实现:
class CFoo
{
public:
bool operator<(const CFoo& f)const
{
return m_num < f.m_num;
}
private:
int m_num = 0;
};
使用友元的一个典型案例是将自定义类型作为map的key:
class classcomp;
class CItem
{
public:
CItem(int n) : m_num(n){}
int getNum()const{ return m_num; }
friend classcomp; //将classcomp设置为CItem的友元
private:
int m_num;
};
class classcomp {
public:
bool operator() (const CItem& lhs, const CItem& rhs) const
{
return lhs.m_num < rhs.m_num; //需要访问CItem类的私有成员
}
};
int main()
{
std::map<CItem, char, classcomp> m;
m.insert({ {2}, 'a' });
m.insert({ {1}, 'b' });
m.insert({ {4}, 'c' });
m.insert({ {3}, 'd' });
//输出为1、2、3、4
for (std::pair<CItem, char> item : m) {
std::cout << item.first.getNum() << std::endl;
}
}
12、类模板
对于两个或多个类,其功能是相同的,仅是数据类型不同,则可以声明一个通用的类模板,其数据类型用一个虚拟的类型来表示,如:
//模板类的声明
template <class TYPE> class Compare
{
public:
Compare(TYPE a, TYPE b):x(a), y(b){}
TYPE max(){ return (x>y)?x:y;}
TYPE min(){ return (x<y)?x:y;}
private:
TYPE x, y;
};
.......
//定义类的对象
Compare<int> cmp1(4, 7);
类模板中方法的实现应该在头文件中,如果要在cpp文件定义成员函数的话,则应当指定模板的类型:
/*CFoo.h*/
template <typename T>
class CFoo
{
public:
T my_max(T x, T y);
};
/*CFoo.cpp*/
template <typename T>
T CFoo<T>::my_max(T x, T y)
{
if (x > y)
return x;
else
return y;
}
template int CFoo<int>::my_max(int x, int y);
template double CFoo<double>::my_max(double x, double y);
类模板中虚拟的参数类型可以有多个,如:
template <class TYPE1, class TYPE2> class Someclass
{
......
};
Someclass<int, float> soc1(3, 4.1);
一个类模板可以作为基类,派生出派生模板类。
利用模板还可以来表示一个虚拟的,通用的数值,对象等,eg:
template<int N>
class CDemo
{
public:
int TestFunc(int x)
{
return x * N;
}
};
.......
CDemo<10> dm;
int iRet = dm.TestFunc(5); // iRet为50
现在有一个通用的数据类,虚拟类型有两个,传入类型和返回类型,声明的时候可以这样template <typename T, typename R> class CData{......}。也可以不在模板中定义多个虚拟类型,改为使用using,如下所示的CData:A类请求使用SRequestA类型的传入参数,返回类型为SReturnCommon,B类请求使用SRequestB类型的传入参数,返回类型为SReturnCommon,...:
template <typename T>
class CData
{
public:
using reqT = typename T::SRequest;
using retT = typename T::SReturn;
retT request(const reqT& in) {
retT ret = {};
...... //设置ret
return ret;
}
};
struct SRequestA
{
SRequestA(const std::string& i) :id(i){}
std::string id;
};
struct SRequestB {
SRequestB(const std::string& t) : time(t) {}
std::string time;
};
...
struct SReturnCommon {
long long ll;
double fi;
};
struct TypeA {
using SRequest = SRequestA;
using SReturn = SReturnCommon;
};
struct TypeB {
using SRequest = SRequestB;
using SReturn = SReturnCommon;
};
...
int main(int argc, char** argv)
{
auto sp1 = std::make_shared<CData<TypeA>>();
sp1->request(CData<TypeA>::reqT("id"));
auto sp2 = std::make_shared<CData<TypeB>>();
sp2->request(CData<TypeB>::reqT("time"));
...
}
上面使用using实现多个虚拟类型的好处就是,可以针对不同的使用场景定义不同的request()方法来使用,如下所示,对于A业务我们使用的是SRequestA传入类型,请求方法reques()中使用的是A方法,对于B业务我们使用的是SRequestB传入类型,请求方法reques()中使用的是B方法,... :
template <>
inline CData<TypeA>::retT CData<TypeA>::request(const CData<TypeA>::reqT& in)
{
CData<TypeA>::retT ret = {};
......//通过A方法设置ret
return ret;
}
template <>
inline CData<TypeB>::retT CData<TypeB>::request(const CData<TypeB>::reqT& in)
{
CData<TypeB>::retT ret = {};
......//通过B方法设置ret
return ret;
}
...
int main(int argc, char** argv)
{
auto sp1 = std::make_shared<CData<TypeA>>();
sp1->request(CData<TypeA>::reqT("id"));
auto sp2 = std::make_shared<CData<TypeB>>();
sp2->request(CData<TypeB>::reqT("time"));
...
}
三、运算符重载
1、运算符重载的方法是定义一个运算符重载函数,而关键字operator用来定义运算符重载函数,如:
int operator+(int)
{
......
}
其中,operator+为运算符重载函数的函数名。
运算符重载的规则:1、不能重载的运算符有“.”,“::”,“?:”,“sizeof”。
2、重载不能改变运算符运算对象的个数、运算符的优先级、运算符的结合性。
3、运算符重载函数不能有默认的参数。
4、用于类对象的运算符一般都需要重载,而“=”和“&”则不用重载。
5、运算符重载函数可以是类的成员函数、友元函数、普通函数。
6、一般讲双目运算符重载为友元函数,将单目运算符重载为类的成员函数。
一般我们将双目运算符的重载声明为类的友元函数,将单目运算符声明为类的成员函数,如以下为重载complex类“+”和“后++”运算符的实现:
class complex
{
public:
complex(int n1, int n2):num1(n1),num2(n2){}
friend complex operator+(complex& c1, complex& c2);
complex operator++();
private:
int num1, num2;
};
complex complex::operator++()
{
complex temp(*this);//对象的复制
num1++;
num2++;
return temp;
}
complex operator+(complex &c1, complex& c2)
{
return complex(c1.num1+c2.num1, c1.num2+c2.num2);//无名对象
}
2、隐式类型转换:
int i = 6;
i = i + 7.5;</span>
显示类型转换:
int i = 6;
i = i + int(7.5);//i = i + (int)7.5;</span>
3、转换构造函数作用是将一个其他类型的数据转换成类的对象,转换构造函数只有一个参数,如以下转换构造函数的作用是将dobule型参数r转换为Complex类的对象:
class Complex
{
public:
Complex(int r)
{
real = r;
imag = 0;
}
private:
int real;
int imag;
};
Complex C = 10;//隐式调用了转换构造函数
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。 一是个构造器 ,二是个默认且隐含的类型转换操作符。如果我们不想要这种隐式的转换而是仅把一个参数的构造函数当做普通的构造函数的话可以使用explicit关键字来声明一个参数的构造函数。eg :
explicit Complex(int r);
C++类中的四种构造函数:默认构造函数、用来初始化的普通构造函数、复制构造函数、转换构造函数。
4、类型转换函数的作用是将一个类的对象转换成另一类型的数据,类型转换函数没有函数类型和参数,且只能是类的成员函数。
类型转换函数与运算符重载函数类似,不同的是类型转换函数的operator后面跟的是数据类型,运算符重载函数operatro后面跟的是运算符,且运算符重载函数有函数类型和参数。
如以下的operator int()函数就是类型转换函数:
......
operator int()
{
return real;
}
......
int i = 6;
i = i + c1;//没有重载运算符+的话,c1对象会被转换为int类型的real数据成员</span>
四、一个C++类示例
class MyString
{
public:
MyString(const char* str = NULL);//转换构造函数
MyString(const MyString& another);//拷贝构造函数
~MyString();//析构函数
MyString& operator=(const MyString &rhs);//重载赋值操作符=
operator int(); //类型转换函数
private:
char* m_pData = nullptr;
int m_Num = 0;
};
MyString::MyString(const char *str)
{
if (str)
{
m_pData = new char[strlen(str) + 1];
strcpy_s(m_pData, strlen(str) + 1, str);
}
else
{
m_pData = NULL;
}
}
MyString::MyString(const MyString &another)
{
if (another.m_pData)
{
m_pData = new char[strlen(another.m_pData) + 1];
strcpy_s(m_pData, strlen(another.m_pData) + 1, another.m_pData);
}
else
{
m_pData = NULL;
}
}
MyString::~MyString()
{
if (m_pData)
delete[] m_pData;
}
MyString& MyString::operator=(const MyString& rhs)
{
if (&rhs == this)
return *this;
if (m_pData)
delete[] m_pData;
if (rhs.m_pData)
{
m_pData = new char[strlen(rhs.m_pData) + 1];
strcpy_s(m_pData, strlen(rhs.m_pData) + 1, rhs.m_pData);
}
else
{
m_pData = NULL;
}
return *this;
}
MyString::operator int()
{
return m_Num;
}
int main()
{
MyString s1("c++"), s2("java"); //直接初始化:调用转换构造函数
MyString s4 = "c#"; //复制初始化:先复制出一个临时的MyString对象,再调用复制构造函数
MyString s3 = s1; //调用复制构造函数
s4 = s1; //调用operator=
s4 = "php"; //先调用转换构造函数,再调用operator=
int n = 10 + s1; //调用operator int
return 0;
}
对于上面的代码需要注意的一点: 对于MyString s4 = "c#"这种操作,c++允许编译器使用直接初始化调用转换构造函数而不是复制初始化,省去了复制临时对象。
枚举类
原来的枚举我们可以直接使用枚举内定义的值,这样就有可能出错,使用枚举类的话必须指明枚举所在的类。
原来的枚举类型不是类型安全的,可以将枚举值直接赋给一个int类型,使用枚举类的话枚举值只能赋给对应的枚举类型,需要赋给int类型的话需要使用static_cast。
原来的枚举类型其实是整形,使用枚举类的话可以指定使用的枚举类型,默认也为int(不过也只能为char、short、long等整形)。
enum eType1{ eAdd, eModify, eNone};
enum class EType2{ eAdd, eModify, eNone };
enum class EType3 : char{ eAdd, eModify, eNone};
int main()
{
int n1 = eAdd;
EType2 n2 = EType2::eAdd;
int n3 = static_cast<int>(EType2::eModify);
}
左值和右值
左值可以出现在赋值=操作的左边,也可以出现在=的右边,右值只能出现在=的右边。
左值是一个能够指向内存地址的表达式,允许我们通过&操作符来获取那块内存地址,右值则不能。
RAII
RAII是资源获取初始化(Resource Acquisition Is Initialization)的简称,它是一种利用对象生命周期来控制程序资源(如内存、互斥量、文件句柄等)的简单技法。
比如我们经常new内存后忘记了delete,所以我们可以不直接使用new,而是将new包装在一个类的构造函数中,将delete包装在其析构函数中。然后我们通过构造这个类的对象来申请内存,当类的对象结束生命周期的时候会自动释放内存:
template<typename T>
class CRAII_new
{
public:
CRAII_new()
{
m_ptr = new T;
}
CRAII_new(int size)
{
m_ptr = new T[size];
m_bAry = true;
}
virtual ~CRAII_new()
{
if (m_ptr)
{
if (m_bAry)
delete m_ptr;
else
delete[] m_ptr;
m_ptr = nullptr;
}
}
public:
operator T*()
{
return m_ptr;
}
private:
T* m_ptr;
bool m_bAry = false;
};
int main()
{
CRAII_new<int> n;
int* p = n;
*p = 100;
cout << *p << endl;
CRAII_new<char> str(100);
char* pStr = str;
strcpy_s(pStr, 100, "hello");
cout << pStr << endl;
return 0;
}
C++中的一些关键字:
volatile:禁止编译器优化,使对变量的访问会从内存重新读取,eg:volatile int iNum = 0。当开启编译器优化后,当前程序可能察觉不出该变量已被程序控制之外所改变,所以一直是从寄存器读取该变量的值,即当变量值改变时也不会从内存中更新该变量。除非是搞嵌入式开发对硬件进行操作,一般我们不会用到它。
__super:调用函数的时候前面加__super::表示调用父类中的同名函数,eg:__super::FuncName();
mutable:在常成员函数中只能访问而不能修改成员变量,如果将成员变量的声明加上mutable,那么在常成员函数中也能修改该变量,eg:mutable int iNum = 0。使用mutable关键字比较常见的一种情况是在一个方法内我们对绝大部分成员都不想要修改其值,但只会修改一个成员变量的值,这个时候就可以将该方法声明为常成员函数,然后将该成员变量声明为mutable。使用mutable的另一种情况是需要在lambda中修改捕获变量的值的情况:
int n = 0;
auto func = [n]()mutable {
n = 100;
};
noexcept:表示函数不会抛出异常:
void f1() noexcept; //声明函数不会抛出异常
void f2() noexcept(true); // 等价于 f1
void f3() noexcept(false); // f3 可能会抛出异常
void g1() { throw "exception"; }
void g2() noexcept { g1(); } //在noexcept方法中抛出异常,进程直接abort结束,不会抛出异常信息
final:用于虚函数表明子类不能再重写该虚函数,用于类表示子类不能再继承自该类。
explicit:用来声明类的构造函数,使一个参数的构造函数是普通的构造函数而非转换构造函数,如下的CFoo类中一个参数的构造函数就是转换构造函数,有了它我们就可以直接将一个int赋值给CFoo对象,但如果使用explicit声明了该构造函数的话则不能直接赋值:
class CFoo
{
public:
CFoo(){}
/*explicit */CFoo(int i){}
};
CFoo t = 1;
= default:启用默认构造函数 ,因为当我们提供了自己的构造函数后默认构造函数就不自动生成给我们了。
= delete:定义删除的函数,删除的函数就不能再调用它。一般我们不会将析构函数定义为删除的函数,否则该类的对象无法释放,eg:
class CFoo
{
public:
CFoo() = default; //显示要求编译器生成默认构造函数。
CFoo(int i) {} //自定义的构造函数
CFoo(const Foo&) = delete; //阻止拷贝
Foo& operator=(const CFoo&) = delete; //阻止赋值
};
constexpr: 与const功能类似,不同的是constexpr声明的变量只能被常量或常量表达式来初始化:
int iNum1 = 0;
const int iNum2 = 0;
//正确
const int iValue = iNum1;
const int iValue0 = iNum2;
const int iValue1 = iNum1 + 1;
const int iValue2 = iNum2 + 1;
constexpr int iValue3 = iNum1; //错误
constexpr int iValue5 = iNum2; //正确
constexpr int iValue4 = iNum1 + 1; //错误
constexpr int iValue6 = iNum2 + 1; //正确
using:using除了搭配namespace来使用(如using namespace std;)外,也可以像typedef那样来定义别名,如using uint64 = unsigned long long; 相当于是typedef unsigned long long uint64; 。而且using支持为模板类设置别名,typedef则不行:
template <typename T>
typedef std::vector<T> v;//编译出错
template <typename T>
using v = std::vector<T>;
而且using也可以仅使用别名,而不定义别名:
namespace n1{
using uint64 = unsigned long long;
}
namespace n2 {
using n1::uint64;
uint64 func(){}
}
初始化列表
C++11扩大了初始化列表的适用范围,使用初始化列表时,可添加=,也可不添加:
int num = 0;
int num(0);
CFoo f = CFoo(100, 10);
CFoo f(100, 10);
int num = { 0 }; //列表初始化
int num{ 0 }; //列表初始化
CFoo f = { 100, 10 }; //列表初始化
CFoo f {100, 10 }; //列表初始化
当初始化列表用于内置类型的变量时,如果我们使用列表初始化值存在丢失信息的风险,则编译器将报错:
double ld = 3.1415926536;
int a = { ld }; //编译出错
C++11提供了initializer_list类型来使方法中能使用初始化列表类型,比如map和set的insert()方法:
void map::insert( std::initializer_list<value_type> ilist );
void set::insert( std::initializer_list<value_type> ilist );
这样我们使用map、set的insert()方法并传入一个初始化列表的时候就表示插入一个map和set,而不是插入一个元素,这里要特别注意!如下所示:
class CItem {
public:
CItem(int n, int m = 0) {
m_n = n;
m_m = m;
}
bool operator<(const CItem& item)const {
return this->m_n < item.m_n;
}
private:
int m_n = 0;
int m_m = 0;
};
int main()
{
std::set<CItem> s1;
s1.insert({ 2, 100 }); // set中元素为 CItem(2, 0)、CItem(100, 0),而不是CItem(2, 100)
std::set<CItem> s2;
s2.insert({ {2, 100} }); //set中元素为CItem(2, 100)
}
#define
#define JUCE_DECLARE_NON_COPYABLE(className) \
className (const className&) = delete;\
className& operator= (const className&) = delete;
#define JUCE_DECLARE_WEAK_REFERENCEABLE(ClassName) \
struct MyComponent : public Component<ClassName>::Master { ~MyComponent() { this->clear(); } }; \
MyComponent m_com; \
friend class OtherComponent<ClassName>; \
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className) \
JUCE_DECLARE_NON_COPYABLE(className) \
JUCE_DECLARE_WEAK_REFERENCEABLE(className)
class CBar
{
......
private:
......
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CBar)
};