C++基础提升及实战
1 头文件防卫式声明:
为了避免同一个头文件
line.h
在代码中被引用
#include
多次,C/C++ 中有两种方式宏实现头文件防卫式声明:
1.1
#ifndef
方式:
基本形式
宏定义名
_LINE_H
是可以自由命名的,但每个头文件的宏定义名都应该是唯一的,一般将宏定义名命名为头文件名的全大写形式,并以下划线为前缀,把文件名中的"."替换为下划线(
line.h
变
__LINE_H
);
#ifndef
方式将头文件的内容包括在其中,具体形式如下:
#ifndef _LINE_H
#define _LINE_H
#include<iostream>
using namespace std;
class human
public:
std::string GetName();
int GetAge();
private:
std::string mstrName;
int miAge;
#endif
#ifndef
方式的优点:
-
#ifndef
属于 C/C++ 语言特性,可移植性较好,各个编译器都能支持; -
#ifndef
不但可以针对整个文件内容,也可以针对文件中的具体一段代码;
//假设程序要暂时跳过一个代码块,但不从文件中删除该代码块
#include<iostream>
int main()
#define _SKIP_
#ifndef _SKIP_
/*要跳过的代码*/
std::cout << "这里被跳过" << std::endl;
#endif
std::cout << "这里被执行" << std::endl;
return 0;
}
- 既可以保证某一头文件不会被包含多次,又能保证内容完全相同的两个文件不会被同时包含;
#ifndef
方式的缺点:
- 如果不同头文件中的宏定义名相同时,可能就会导致头文件明明存在,但编译器却硬说找不到声明的状况;
- 由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长;
1.2
#pragma once
方式:
基本形式:
在头文件的第一行加上
#pragma once
#pragma once
#include<iostream>
using namespace std;
class human
public:
std::string GetName();
int GetAge();
private:
std::string mstrName;
int miAge;
};
#pragma once
方式的优点:
- 更高效,因为它不需要打开文件,就可以判断这个文件有没有被包含;
-
因为没有使用宏定义名,所以不存在
#ifndef
方式的宏定义名冲突,也就不会引发相应的错误;
#pragma once
方式的缺点:
- 属于编译器提供的指令,一些老的编译器不支持,可移植性较差;
- 不能对一个头文件中的某一段代码作#pragma once声明,只能针对文件;
- 只能保证同一个文件不会被引用多次,但是当引用了两个内容相同的不同名文件时,仍会出现重复引用错误;
2 默认构造函数:
默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:
- 没有带明显形参的构造函数。
- 提供了默认实参的构造函数。
强调“没有带明显形参”的原因是,编译器总是会为构造函数形参表插入一个隐含的this指针,所以”本质上”是没有不带形参的构造函数的;
如果没有显式的写出任何构造函数,那么编译器将自动生成一个默认的无参构造函数,一旦你在类中定义了构造函数,那么编译器将不再生成默认的构造函数。
4 常对象与常成员函数:
4.1 常对象:
指对象的数据成员不能被修改;一旦将对象定义为常对象之后,就只能调用类的
const
成员(包括
const
成员变量和
const
成员函数);
- 常对象调用常成员函数 可以
- 常对象调用普通成员函数 不行,常量对象一旦初始化后,其值就再也不能更改;因此,不能通过常量对象调用普通成员函数,因为普通成员函数在执行过程中有可能修改对象的值;
4.2 常成员函数:
常成员函数:指由
const
修饰符修饰的成员函数,在常成员函数中不得修改类中的任何数据成员的值;
- 普通对象调用常成员函数 可以
- 常成员函数调用常成员函数 可以
- 常成员函数调用非常成员函数 不行(静态成员函数除外,常成员函数可以调用静态成员函数)
两个成员函数的函数名和参数相同,但一个为
const
,一个不是,则它们构成重载;
普通函数(非类的成员函数)不能定义为
const
;
#include<ostream>
using namespace std;
class OBJ {
public:
void Print() { }
int main()
const OBJ obj;
obj.Print(); //error, 不能通过常量对象调用普通成员函数
return 0;
}
5 常指针与指向常量的指针:
根据const修饰的类型的不同来判断,const修饰p就是常指针,const修饰*p就是指针常量;
5.1 常指针:
int* const p
指针变量所指的空间地址是常量,不可以重新指向其他地址;
const修饰的时p, p是int* 类型的指针,即const修饰的时指针,即指针不能指向新的地址;
int* const p = new int;
*p = 10; //ok
p++; //error
5.2 指针常量:
const int* p;
int const *p;
指针指向的为常量,指针可以指向其他地址,但是该常量的值不能修改;
const修饰的是*p, 即指针指向指向地址的内容是常量;
const int* p = new int;
p++; //ok
*p = 23; //error
6 指针的空间分配:
Time *pt;
只是定义了指向
Time
类对象的指针变量
pt
,并没有实例化对象;
定义一个指向对象的指针有两种方法:
Time t;//实例化Time类对象t
Time *pt=&t;//定义pt为指向Time类对象t的对象指针
或者
Time *pt = new Time();
7 成员函数的隐藏参数-this指针:
在 C++ 中,每一个对象都能通过
this
指针来访问自己的地址,调用成员函数时会将对象的地址作为实参传递给
this
指针,
this
指针是所有成员函数的隐含参数,因此,在成员函数内部,它可以用来指向调用对象; 对于成员函数
Student::InitStudent(char * name,char *gender,int *age);
,如果显式的写出this指针,则为
Student::InitStudent(Student* const this,char * name,char *gender,int *age);
;
友元函数没有
this
指针,因为友元不是类的成员;只有成员函数才有
this
指针;
this
是
const
指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的;
this
指针只能在成员函数内部使用,用在其他地方没有意义,也是非法的;
只有当对象被创建后
this
才有意义,因此不能在
static
成员函数中使用;
8 初始化列表:
构造函数初始化列表:
构造函数():变量名1(数值),变量名2(数值){}
构造函数的初始化列表需要和数据成员声明的顺序保持一致,不然会有warning或者意想不到的错误;
类的常成员变量只能只用初始化列表初始化,而不能在构造函数函数体内进行赋值,如下:
#include<iostream>
using namespace std;
class human
public:
human(string name, int age):mstrName(name),miAge(age)
//warring
//human(string name, int age):miAge(age),mstrName(name)
//error
//human(string name, const int age)
// miAge = age;
// mstrName = name;
void info()
cout << mstrName << " is " << miAge << " years old." << endl;
private:
const string mstrName;
int miAge;
int main()
human xiaoming("xiaming",18);
xiaoming.info();
return 0;
}
C++ 11 列表初始化容器: C++11的新特性
initializer_list
用来做函数的参数,函数可以接收可变长度的传参;
#include<iostream>
using namespace std;
void print(initializer_list<int> list)
for (auto it = list.begin(); it != list.end(); ++it)
cout << *it << " ";
cout << endl;
int main()
print({ 1,2,3,4,5,6,7 });
return 0;
}
9 友元函数与友元类:
9.1 友元:
朋友是值得信任的,所以可以对他们公开一些自己的隐私;
友元破坏了类的封装性,尽量少用
类A的友元B(友元函数、友元类)有权访问类A的所有私有(private)成员 和保护(protected)成员;
友元不属于
public
、
private
或者
protected
,友元的声明可以出现在类的任何地方,包括在private和public部分;
友元不具有传递性:类
A
是类
B
的友元,类
B
是类
C
的友元,并不能导出类
A
是类
C
的友元;“咱俩是朋友,所以你的朋友就是我的朋友”这句话在 C++ 的友元关系上是不成立的,具体如下:
#include <iostream>
class A
private:
int a;
public:
friend class B;
class B
friend class C;
public:
void fun(A& ob)
std::cout << ob.a << std::endl;//ok,可以访问
class C
public:
void fun(A& ob)
std::cout << ob.a << std::endl;//error,不能访问
friend class B;
};
9.2 友元函数:
在类的定义中函数原型前使用关键字
friend
,可以把一些函数(包括全局函数和其他类的成员函数)声明为该类的友元函数,在友元函数内部就可以访问该类对象的所有私有
private
成员和保护
protected
成员;
尽管友元函数的声明有在类的定义中出现过,但是友元函数并不是类的成员函数;
9.3 友元类:
类
A
将类
B
声明为自己的友元,那么类
B
的所有成员函数就都可以访问类
A
对象所有私有成员;
友元访问私有成员的方式: 通过在友元函数的形参中定义对象来访问,具体如下:
#include <iostream>
class A
private:
int a;
public:
friend class B;
class B
friend class C;
public:
void fun(A& ob)//通过在形参中声明对象来访问;
std::cout << ob.a << std::endl;//访问类A的私有成员
};
9.4 友元的继承问题:
- 友元关系不能被继承
-
类
A
是类B
的父类,类A
是C
的友元,则类B
不是类C
的友元; -
类
A
是类B
的父类,类C
是A
的友元,则类C
是类B
的友元;(此处有争议)
#include <iostream>
class Father
friend class Father2;
protected:
int i;
class Son : public Father
protected:
int j;
class Father2
public:
int mem(Father obj) { return obj.i; } // ok
int mem(Son obj) { return obj.i; } // ok,此处有争议
class Son2 : public Father2
public:
int mem(Father obj) { return obj.i; } // error,友元关系不能被继承
};
10 拷贝构造函数与赋值运算符重载
赋值运算符重载是将已存在的对象赋值给新的对象;而拷贝构造函数会直接创建一个新的对象。
10.1 基本形式
拷贝构造函数重载:
赋值运算符重载:
- 当没有自定义赋值运算符重载函数时,编译器会自动生成一个默认的赋值运算符重载函数;
- 当没有自定义拷贝构造函数时,编译器会自动生成一个默认的拷贝构造函数;
10.2 调用时机:
#include<iostream>
using namespace std;
class human
public:
human()
human(int data):miData(data)
~human()
human(const human& obj)
miData = obj.miData;
cout << "拷贝构造函数重载" << endl;
human& operator= (const human& obj)
miData = obj.miData;
cout << "赋值运算符重载" << endl;
return *this;
void print()
cout << miData << endl;
private:
int miData;
int main()
human obj1(1);
human obj2(2);
obj2.print();
//在对象obj2已经存在的情况下,用obj1来为obj2赋值,调用的是赋值运算符重载函数
obj2 = obj1;
obj2.print();
human obj3(3);
//用obj3来初始化obj4,调用的是拷贝构造函数
human obj4 = obj3;
obj4.print();
human obj5(5);
//用obj5来初始化obj6,调用的是拷贝构造函数
human obj6(obj5);
obj6.print();
return 0;
}
10.3 注意事项
保障连续赋值:
赋值运算符重载的返回值为
human&
类型,才能保证连续赋值;
#include<iostream>
using namespace std;
class human
public:
human()
human(int data):miData(data)
human& operator= (const human& obj)
miData = obj.miData;
cout << "赋值运算符重载" << endl;
return *this;
void print()
cout << miData << endl;
private:
int miData;
int main()
human obj1(1);
human obj2(2);
human obj3 = obj2 = obj1;
obj3.print();
return 0;
}
避免自我赋值引发的问题:
上述的赋值运算符重载函数仍有纰漏,自我赋值在某些情况下会导致内存重复释放的问题,具体待后续补充; 如下,通过添加if来避免自我赋值时带来的问题;
human& operator= (const human& obj)
if(*this == obj)
return *this;
miData = obj.miData;
cout << "赋值运算符重载" << endl;
return *this;
}
11 输入输出运算符重载:
11.1 需声明为友元函数:
输入输出运算符重载均要定义为
friend
,因为成员函数的第一个参数为隐藏的
this
指针,友元函数参数中无this指针,而输出输出运算符重载要求其第一个参数为输入输出流,所以定义为友元函数才能满足条件;
声明为友元函数时,重载函数要求的传参顺序为cin, obj,相当于operator>> (cin, obj),则使用时写为cin >> obj;
若不声明为友元函数,则重载函数要求的传参顺序为*this, cin,相当于operator>> (obj, cin),则使用时需写为obj >> cin;显然这并不符合我们的书写习惯,所以输入输出运算符重载均要定义为
friend
;
11.2 基本形式
C++ 中输入运算符的重载第一个参数是输入流对象的引用,第二个参数是需要输入的类对象的引用,返回值是输入流istream&;不同于输出运算符重载,对于输入运算符重载,将第二个参数定义为非const类型,否则无法赋值;
C++ 中输出运算符的重载第一个参数是输出流对象的引用,第二个参数是需要输出的类对象的引用,返回值是输入流ostream&;
ostream& operator<<(ostream& out,const A& a)
out << miX << miY << endl;
return out;