C++基础

1. 经典头

1
2
#include<iostream> //预编译
using namespace std;

iostream是C++用于管理输入和输出的头文件,包含了在控制台、文件中输入输出的类和函数。 std是C++定义的一个命名空间,cin、cout都在此命名空间中管理,两种输出方式:

1 声明命名空间:using namespace std;

2 域解析符号: std::cout<<“hello world”<<endl;

2. 面向对象编程语言的三要素:

封装:把客观事物封装成抽象的,而且仅让可信的类或者对象进行操作;将成员函数和成员变量封装在内部,根据需要设置访问权限。通过成员函数管理内部状态。如C语言结构体内不能定义函数,而C++可以。

继承:一个类可以继承另一个类的成员函数和成员变量,复用性大大增加

多态:也即一个接口,多种方法;程序在运行时才决定调用的函数,是面向对象的核心概念。分为静态多态(函数重载、运算符重载)和动态多态(虚函数、纯虚函数、虚析构函数、纯虚析构函数);

3. 域解析符(::)

除了用于类内解析,还可以用于全局变量的引用操作。

1
2
3
4
5
6
int a=1000;
int main(){
int a=99; //局部变量地址在栈区
cout<<a<<endl;
cout<<::a<<endl;//全局变量地址在数据段全局区,域解析符调用
}

4. 命名空间

C++支持用户定义自己的命名空间,防止协作编程时名称混用导致出错。

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
namespace eden {   //第一个命名空间eden
int num=100;
void test(){cout<<"hello eden"<<endl;
}
}
namespace mo { //第二个命名空间mo
int num=99; //允许同名变量、函数
void test(){cout<<"hello mo"<<endl;}
}
void test1(){
cout<<eden::num<<endl; //域解析符调用特定空间的变量、函数
cout<<mo::num<<endl;
eden::test();
mo::test();
}
void test2(){
using eden::test; //声明使用函数
test(); //直接使用
using mo::num;
cout<<num<<endl;
}
int main()
{ test1();
test2();
}

5. 结构体的使用

C语言虽然也有结构体,但是C语言的结构体封装性差,对结构体内成员保护性、保密性不足,只要编译器认为数据类型匹配、语法合格即可执行,如定义一个结构体形参,将一个异名结构体传入函数一样能够编译执行:

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
#include <stdio.h>
struct Boy{ //Boy结构体
char colour[32];//定义32字节的char数组
};
struct Girl{ //Girl结构体
char colour[32];
};
void print_boy(struct Boy *boy){ //注意形参是Boy结构体
printf("Boy is %s\n",boy->colour);//实例化的boy的colour会传入%s
}
void print_girl(struct Girl *girl){ //注意形参是Girl结构体
printf("Girl is %s\n",girl->colour);
}
void test1(){
struct Boy boy={"Blue"};
struct Girl girl={"Red"};
print_boy(&boy);
print_girl(&girl);//符合结构体格式,正常执行;
print_boy(&girl); //报兼容性警告,但可以执行,C语言不具备封装成类性,导致类Girl的类任意调用。
}
int main()
{
test1();
return 0;
}
g++编译器支持大部分C语言代码,但是由于C++在这方面增加了严格的类型要求,所以这代码是难以执行的。此外,C++增加了在结构体中对函数的支持,因此能如如下一样封装,由于在结构体内封装,内部变量不用形参即可调用函数,而由于形参缺乏外部变量无法调用,大大增加安全性。
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
#include <iostream>
using namespace std;
struct Boy{
char colour[32]; //colour指代地址,故char数组不支持boy.colour="xxx"的成员初始化方式,要使用strcpy函数
void print_boy(){
printf("Boy is %s\n",colour);//实例化的boy的colour会传入%s
}
};
struct Girl{
char colour[32];
void print_girl(){
printf("Girl is %s\n",colour);
}
};
void test1(){
Boy boy={"Blue"};
Girl girl={"red"};
boy.print_boy();
girl.print_girl();
}
int main()
{
test1();
return 0;
}

6. C++指针

记录了一些常用的指针操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;
int main(){

int num=10;
int *a=&num; //指针定义通过取地址符实现
int *b=a; //指针的传递,将a地址给b,a、b是同等效力的指针

cout<<&a<<endl; //指向存着num存储地址的地址
//结果:0x61fe10
cout<<a<<endl; //num变量的存储地址
//结果:0x61fe1c
cout<<*a<<endl; //解num变量地址,即num变量值
//结果:10
cout<<&b<<endl; //b和a都是int*型,与a是等效的,但b的地址是新开辟的
//结果:0x61fe08
cout<<b<<endl;
//结果:0x61fe1c
cout<<*b<<endl;
//结果:10
return 0;
}
C++同类型的赋值会开辟新的内存空间,与Python的内存管理不同。

7. const关键字

7.1 const修饰全局、局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
//const int num=10; //全局变量编译会出错,常量地址在常量区
int main(){
const int num=10; //局部变量编译通过,但是输出仍是常量值
int *a=(int *)&num; //尝试通过指针改变num值,C++严格保证类型一致,需要强转
*a=300;
cout<<*a<<endl;
//结果:300
cout<<num<<endl;
//结果:10
cout<<a<<endl;
//结果:0x61fe14
cout<<&num<<endl;
//结果:0x61fe14
return 0;
}

表明在C++中const修饰的整型变量输出不变(取决于符号常量表)的,对局部变量而言,可以通过地址修改地址指向的值,但是访问变量名num仍为原值(而在C语言中num可以因此被修改输出的值);对全局变量而言不可以通过地址操作const修饰的变量(编译发生错误)

C++的const是通过符号常量表进行值查询和输出,这有利于提高效率。const的目的是提高效率,优化系统,假如不想利用符号常量表优化,可使用volatile修饰,表明该变量在系统中是易变,在全局、局部变量中修改均生效,可以通过指针修改const的值:

1
const volatile int  num=10;

7.2 const修饰指针变量类型()、指针变量

注意const和指针的相对位置,判断不能修改的是指针变量(如int* const a,称指针常量),还是指针变量类型(如const int* a,称常量指针)

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
#include <iostream>
using namespace std;
int main(){

int num=10;
const int *addr1=&num;//常量指针:addr1地址指向的整型变量的值是不可修改的(值传递可以,指针修改不可)
printf("*addr1=%d\n",*addr1);
printf("addr1=%d\n",addr1);

int num1=100;
num=1;//合法修改,直接修改变量是允许的
addr1=&num1; //合法修改,成为另一个变量的地址指针
// *addr1=100;//非法修改,即不能通过指针修改变量的值
printf("*addr1=%d\n",*addr1);
printf("addr1=%d\n",addr1);

int *const addr2=&num; //指针常量:const修饰指针变量
printf("*addr2=%d\n",*addr2);
printf("addr2=%d\n",addr2);

int num2=200;
num =100; //合法修改,直接修改变量仍是允许的
*addr2=100; //合法修改,能通过指针修改变量的值
//addr2=&num2; 非法修改

printf("*addr2=%d\n",*addr2);
printf("addr2=%d\n",addr2);

return 0;
}

7.3. const与#define区别

  1. const有类型而#define无类型,前者可进行编译器类型检查
  2. const有作用域而#define不重视作用域,默认是定义到文件结尾,如果指定作用域宜使用const。

8. 宏函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

#define COMPARE(a,b) a>b?a:b //宏函数定义

int main(){
int a=20;
int b=20;
int sum=COMPARE(++a,b);
cout<<"max="<<sum<<endl;
return 0;
}

结果22

宏函数本质就是宏定义,在预处理阶段自动替换:++a本身直接替换,20变21,判断结束仍为++a,21变22;

9. 内联函数

内联函数是用inline修饰的函数,运行结果实际上和普通函数调用没有区别,只是在编译过程某些地方会以预定义宏的方式展开,而不像普通函数调用带来额外的开销。简短且多次调用的函数编译器会自动将其看成内联函数,复杂冗长的函数即使加上inline,编译器也不会将其看成内联函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

inline int ComMax(int a,int b){
return a>b?a:b;
}
int main(){
int a=20;
int b=20;
int num=ComMax(++a,b);
cout<<"max="<<num<<endl;
return 0;
}

结果max=21
++a作为内联函数的实参,直接以21带入内联函数里,因此结果为21。

10. 函数的默认参数

在函数的形参部分可以赋予初始值当作默认值,调用时可省略或者修改参数

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

void Record(string name,int id,int score=61,string school="五道口"){
cout<<name<<'\t'<<id<<'\t'<<score<<'\t'<<school<<endl;
}

int main(){
Record("eden",99);
return 0;
}
注意:默认参数应遵循形参从右到左连续赋值,以便编译器能够辨认区分。

11. 函数重载

C语言中函数地址完全取决于函数名同名函数无法共存C++函数地址取决于函数名和参数,只要使用不同个数的形参、或不同数据类型的形参、或不同命名空间管理函数,同名函数可以共存。

Attention:C++中函数重载与默认值调用引起的歧义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>  //注意这是一个非法程序
using namespace std;

int Sums(int a,int b,int c=888){
return a+b+c;
}

int Sums(int a,int b){

return a+b;
}
int main(){
Sums(99,100);
return 0;
}

根据函数重载条件,两个函数可同时存在,但由于三个参数的Sums使用了默认参数,导致函数调用可同时调用这两个函数,引起冲突。

12. 引用

C++继承C指针的应用,还引入了引用机制。引用的作用是为变量地址起一个别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main(){

int a=100;
int &b=a;

cout<<"Address:"<<&a<<'\t'<<&b<<endl;
cout<<"Num:"<<a<<'\t'<<b<<endl;

b=10;

cout<<"Address:"<<&a<<'\t'<<&b<<endl;
cout<<"Num:"<<a<<'\t'<<b<<endl;
return 0;
}

结果:Address:0x61fe14 0x61fe14 Num:100 100 Address:0x61fe14 0x61fe14 Num:10 10

可见引用和原地址的效果是完全等效的。

12.1 引用的特点:

  1. 指针变量开辟了新空间,用于存储变量的地址,而引用的作用是为变量地址起一个别名,本身并不开辟空间
  2. 引用必须初始化,必须是对已有的空间进行赋别名不能仅声明,而指针变量是允许的。
  3. 引用只能初始化一次,初始化之后不能作为其他空间别名

12.2 引用的应用场景:

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
34
35
36
37
38
#include <iostream>
using namespace std;
void exchange1(int a,int b){ //值传递
int temp=a; //此处的地址是形参地址,没有涉及实参
a=b;
b=temp;
}
void exchange2(int *a,int *b){ //地址传递:涉及了实参地址
int temp=*a; //存储了实参地址
*a=*b; //b地址的值替换a地址的值
*b=temp; //替换b地址值,实现了m、n互换
}
void exchange3(int *a,int *b){ //地址传递:涉及了实参地址
int *temp=a;
a=b;
b=temp; //存储了实参地址,但是是对地址复制出来的a、b进行了地址交换,没有涉及m、n值;
}
void exchange4(int &a,int &b){ //引用,a、b是m、n的别名
int temp=a;
a=b;
b=temp; //对调a、b不再是前述exchange1形参操作,而是以a、b为别名的m、n操作
}
int main(){
int m=1;
int n=99;
cout<<&m<<endl;
cout<<&n<<endl;
cout<<"m="<<m<<'\t'<<"n="<<n<<endl;
exchange1(m,n);
cout<<"m="<<m<<'\t'<<"n="<<n<<endl;
//exchange2(&m,&n);
//cout<<"m="<<m<<'\t'<<"n="<<n<<endl;
//exchange3(&m,&n);
//cout<<"m="<<m<<'\t'<<"n="<<n<<endl;
//exchange4(m,n);
//cout<<"m="<<m<<'\t'<<"n="<<n<<endl;
return 0;
}

结果:原输出m=1 n=99;
exchange1后:m=1 n=99,对调无效;基于形参的操作并不影响m、n空间,因此m、n值不会更改,通过值传递对调参数必须加以返回值方式实现对调。
exchange2后:m=99 n=1,对调成功;通过获取地址对值进行修改。 exchange3后:m=1 n=99,对调无效;虽然是地址传递,a、b带有m、n地址,但是交换a、b地址不代表交换m、n地址,没有涉及m、n值操作,应该加上对地址赋值;
exchange4后:m=99 n=1,对调成功;引用是别名,对a、b交换操作就是对m、n操作。

2. 函数的引用作为返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

int &test(){
static int a =100;
return a;
}

int main(){
int &b=test();
cout<<b<<endl;
return 0;
}

static int的作用是使变量a长久存在直到程序结束,使用b引用可以避免新变量带来额外空间开销。值得注意的是,int &类函数执行完成后变量a会销毁,如果不使用static会导致执行失败。

13. 动态开辟空间

13.1 兼容C语言的动态空间方法:malloc、calloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
//C语言在堆区开辟动态空间
void spaceoper1(){
int *a=(int *)malloc(4);
cout<<*a<<endl; //随机数字,不初始化
free(a); //释放空间,但仍可通过*a赋值使用
a=NULL; //防止野指针,无法通过*a使用
}
void spaceoper2(){
int *b=(int *)calloc(1,4); //1块空间,4个字节
cout<<*b<<endl; //初始化为0
free(b);
b=NULL;
}
int main(){
spaceoper1();
spaceoper2();
return 0;
}

此外还有比较少用的realloc():用于在已有的空间中动态增加空间,假如已有空间后续已无可用空间,则会将原来空间复制到新空间,释放原来空间并返回新地址

13.2 C++特有方法:关键字new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int main(){
int *a=new int;
cout<<*a<<endl;
//输出-1163005939,说明new默认不初始化空间值
int *b=new int(100);
cout<<*b<<endl;
//输出100,可通过小括号赋初始值
int *c=new int[4];
cout<<c[0]<<c[1]<<c[2]<<c[3]<<endl;
//输出4个-1163005939,中括号可开辟多个空间,但不同时赋初始值

//释放空间
delete a;
delete b;
delete []c;

return 0;
}

13.3 关于C++数据类型对应字节数:

32位编译器: char:1
short:2
int/long/float/指针:4
double:8

64位编译器
long、指针均改为8位。

14. char指针类型赋值问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include<cstring> //strcpy
using namespace std;
int main(){

char *str=new char[32]; //开辟32个字节空间
printf("%p\n",str); //%p打印指针地址
//str="Hello World";//非法语句把常量字符串地址赋予了字符指针str,这将导致开辟的空间泄露
//printf("%p\n",str);//前后地址不一
//要正确赋值应该采用以下写法
strcpy(str,"Hello World"); //不会改变地址
printf("%p\n",str);

return 0;
}

char指针类型直接赋值会更新地址值,导致丢失原来开辟的空间,因此应该通过strcpy函数进行赋值,整型变量则不存在该问题。

15. char*指针问题

1. 指针变量浅拷贝的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//这是一个非法程序
#include<iostream>
#include<cstring>
using namespace std;

int main(){
char *name1=new char[32];
strcpy(name1,"Hello World");
cout<<name1<<endl;

char *name2=name1; //指针的值传递,浅拷贝,两个指针指向同一块地址
cout<<name2<<endl;
delete []name1;
delete []name2; //浅拷贝释放两次内存导致程序卡死或出错

}
深拷贝解决指针赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<cstring>
using namespace std;

int main(){
char *name1=new char[32];
strcpy(name1,"Hello World");
cout<<name1<<endl;

char *name2=new char[strlen(name1)+1]; //name2开辟一块和name1相当空间,习惯性+1
strcpy(name2,name1); //复制name1内容
cout<<name2<<endl;
delete []name1;
delete []name2; //指向地址不同正常释放
}

2.char*与char[]与string:

在C++中,三个都常被用于定义字符串,如

1
2
3
char *str="Hello Eden";
char str[]="Hello Eden";
string str="Hello Eden";
但涉及C++内存管理,在使用上也有区别: 在char*定义的字符串中,如果像上述一样定义一个字符串,实际上是把常量地址赋予了str,也即我们在1中说明的浅拷贝,这样做以后不仅会引起内存释放的问题,也导致了我们无法直接操作str来修改str内容,因为操作一个常量地址是非法的;另外,两个char*字符要比较是否相等,通常需要使用strcmp或者strncmp函数,返回0代表相等,而不是operator==,因为==比较两个字符是否指向同一块地址空间。

而char str[]则是字符数组,str本身代表数组首元素地址编译器开辟适宜大小,且把常量字符串拷贝到str中,这种情况下则可修改;值得注意的是,有些字符串函数内含了对字符串的修改(如分割函数strtok,见C++第三篇),因此虽然函数参数类型是char *,在定义时却传入char str[],这是因为不能对char *str进行写(分割)操作

string是一种抽象的类,可以看成是升级版的char*的抽象,解决了内存释放、读写等可能遇到问题,提供更鲁棒的功能,在第二篇文章将介绍简单实现原理。

16. C++类型转换

C++对数据类型有着严格要求,很多禁止数据类型操作实际上和非法的内存操作相关,但C++也提供了多种数据类型转换方式和模板。

显式转换和隐式转换

这是比较常见的类型转换,所谓显式就是手动转换隐式就是编译器的自动转换。如

1
2
3
int a=-10;
unsigned int b=2;
a+b; //有符号数和无符号数相加,编译器会自动将有符号数转换成无符号数再计算,如果需要无符号结果则%u,有符号%d;
还有几种特定场合的转换:

静态转换:static_cast

常用于基本数据类型之间的转换(而基本数据类型的指针转换是不允许的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
void test1(){
int a=0x12345678; //32位
char b=a; //char只保留低八位,即0x78,这是隐式调用静态转换
char c=static_cast<char>(a); //显式静态转换s
////char *c=static_cast<char*>(&a); //指针是不允许的
char c=
printf("b=%c\n",b);
printf("c=%c",c);
}
int main()
{
test1();
return 0;
}
父类、子类的指针、引用的转换是允许的;父类、子类的指针转换涉及安全性问题,例如定义指针空间大于原有空间,那么可能会对原有的数据造成损害。我们知道,通过继承,子类占用的空间一般会大于父类,因此指针转换分为上行转换下行转换
1
2
3
4
5
6
7
//小指针大内存,上行转换,父类指针对子类进行操作没有安全问题
Son s1;
Parent *p1=static_cast<Parent *>(&s1);

//大指针,小内存,下行转换,子类指针对父类进行操作存在安全问题
Parent p1;
Son *s1=static_cast<Son *>(&p1);
值得注意的是,使用静态转换时编译器不会对下行转换进行检查,因此某些场合的下行转换使用动态类型转换具有更高的安全性

动态类型转换:dynamic_cast

1
2
3
4
5
6
7
//上行转换允许
Son s1;
Parent *p1=dynamic_cast<Parent *>(&s1);

//下行转换报错
Parent p1;
Son *s1=dynamic_cast<Son *>(&p1);

动态类型转换不支持基本数据类型(包括指针)的转换操作

常量转换:const_cast

允许对常量指针常量引用进行转换,使其转换称非常量指针非常量引用可以被修改和操作

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int main(){
const int a=10;
int *p=(int *)&a;//法一:强类型转换,可能导致未知问题
*p=100; //改a的地址指向值

int *q=const_cast<int*>(&a); //法二:效果与一相同,但操作更安全
cout<<a<<endl; //符号常量表值,仍为10
cout<<*p<<endl; //修改值生效,100;
cout<<*q<<endl; //与p一致,100
return 0;
}

reinterpret_cast:重新解释转换

类似强制类型转换强大却也是最不安全的转换方法,整型、指针可相互转换等,不详叙。

17. static关键字

C语言很常用的关键字,C++中涉及的静态成员函数、静态成员变量实例请参考第二篇文章,这里仅回顾其作用特性和提前概述。

static关键字作用:
1. 对象寿命与整个程序一致,但作用域限定为本文件:在一个.c/.h文件定义一个全局静态变量/全局静态函数,只有程序结束其生命周期才结束,但是其他文件也无法引用或者调用这个变量/函数(解除文件耦合,解决命名重复问题)。

  1. 内存只有一份拷贝,初始化默认为0(未初始化/初始化为0存于bss段,其余初始化存于.data段,均属于静态变量区),而且只会在程序中初始化一次;注意:如果是静态局部变量,作用域仅在函数体内,且该函数被多次调用,静态局部变量仅会在首次调用初始化一次。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void count(){
    static int num; //静态局部变量:仅第一次调用初始化
    num++;
    printf("%d ",num);
    }
    int main(){
    count(); //输出1
    count(); //输出2
    count(); //输出3
    }

  2. C++内容:修饰静态成员变量:属于类,类对象可共享,遵守访问权限,命名属于类,减少命名空间污染;修饰静态成员函数:属于类,无需实例化对象,只能用访问和操作静态成员函数(因为不含this指针)。const、volatile、virtual关键字都是针对类实例对象,和static使用是冲突、没有意义的。