局部变量和全局变量
在一个文件里,在所有函数的外面声明一个变量,如果需要其他的文件也能访问这个变量,使用extern
。
对于其他文件里定义的函数,如果要在另一个文件中调用,则需要在另一个文件中加上前置声明。
int a = 0; // 在文件1.cpp声明;
extern int a; // 在文件2.cpp中使用a变量,需要在前面加extern
如果有很多很多的文件需要使用全局变量,上面声明全局变量和全局函数的方法过于繁琐,可以通过添加头文件的方法,然后对每个文件引用头文件即可。
注意:不能在 h 文件中直接定义全局变量 int sum = 0,这样的话如果有多个C/Cpp文件 include 包含这个头文件的时候就会提示 sum 重复定义了。所以一定要在 C/Cpp文件中定义该全局变量,之后在 h 头文件中声明该全局变量。
// 头文件里的写法,其中,a和myfun()的定义可以在任意一个文件里面
extern int a;
void myfun();
- static 静态类型局部变量只初始化一次,之后的调用都不进行初始化。
数组
所以数组的基本定义格式为:类型名 数组名[常量表达式];注意,括号里面的数是常量,不能用变量来定义。如果需要定义变长的数组,需要使用new
动态申请。
初始化数组的时候,如果只设置数组的长度,不初始化值,建议都初始化为0,即int a[5] = {0}
,否则,如果不给数组初始化值,其中的值是乱七八糟随机的数值,写代码的时候可能会遇到问题。
// 定义固定长度数组
int a[50];
string b[20];
// 定义变长的数组
int num = 100;
int *student = new int[num];
// 初始化数组的三种方式
int a[5] = {1, 4, 65, 56, 3};
int b[5] = {3,4}; // 后面没有初始化的默认为0
int b[] = {1, 3, 4, 45, 5, 0}; // 不指定数组的长度,直接输入数组的元素
字符数组
在cpp中,字符用单引号,字符串用双引号。和数组的初始化类似,未初始化的话,其值为随机值。
字符数组在某些情况下可以当作字符串来用。注意,字符数组和字符串是不太一样的,字符串数组若没有 '\0',只能当做数组处理,若有 '\0',可以看做字符串,可以cou<<text,否则不可以。用字符串初始化字符数组时,系统会在字符数组的末尾自动加上一个字符"\0",因此数组的大小比字符串中实际字符的个数大。
char a[5] = {'a', 'b', 'c', 'd', 'e'}; // 初始化一个字符数组
char a[10] = {'a', 'b', 'c', 'd', 'e'}; // 初始化字符数组,由于后面未初始化的部分为'\0',因此这个也可以当字符串来用
char a[10] = "hello"; // 可以通过双引号直接初始化字符数组,未初始化的部分默认为'\0'
char a[] = {'a', 'b', 'c', 'd', 'e', 'd', 'v'}; // 自动初始化字符数组的长度
// 使用cout输出字符串的时候,如果是一个字符数组(而不是字符串),会输出一些乱七八糟的东西,比如:
char a[5] = { 'd', 'a', 'l', 'u', 'm' };
cout << a << endl;
// 这是因为,cout输出的是字符串,会一直找'\0',才停下
地址与指针
可以通过指针更改变量的值,比如说,将变量传入一个函数,想通过这个函数更改变量的值。
int* pa = 0; \\定义一个指针
int a = 0
int* pa;
pa = &a;
int b = 1;
*pa = b; // 可以给a更改值,此步将a赋值为b的值
数组与指针
数组的名字是指向数组的首地址,即数组第0个元素的地址,并且数组的名称指向的地址不能更换(int * const 类型)。通过将指针一个int来得到数组某个元素前/后面的元素。
int num[100] = { 0 };
int* pnum_0 = &num[0];
int* pnum_1 = &num[1];
pnum_0 += 1 // 此时,pnum_0和pnum_1指向的地址是相同的
// 访问数组的两种方法
num[0]; // 下标法访问数组
*(num + idx) // 指针法访问数组
结构体数组与指针
结构体作为函数参数,默认情况下以值传递的方式传入函数中。所以,
- 如果需要通过函数直接修改结构体,需要以指针的形式传入;
- 另外,如果结构体里的成员非常多,占用空间非常大,输入函数的时候,如果是直接将结构体传入函数,函数在执行的时候会再复制一个结构体,就导致效率降低,这种情况下,也适合将结构体的指针传入函数。
struct student
{
string name[];
int xuehao;
} zhangsan;
zhangsan.name // 访问成员,用“.”这个符号
struct* lisi; // 如果是指针形式的struct
lisi->name; // 可以直接用箭头的形式访问成员
(*lisi).name; // 也可以先将指针前面加上*变成结构本身,然后用.的形式访问成员
枚举类型及定义新的类型名字
可以用 typedef 类型声明新的类型名字:除了可以用 struct 结构体,union 联合体,enum 枚举等自定义类型以外,还可以使用 typedef 声明一个新的类型名字来代替已有的类型名。注意是新的类型名字,只是名字而已,不是一种全新的类型,只是改个名字而已。
typedef unsigned int uint; \\ 重新定义类型unsigned int的名字
enum Eweekday
{
Eweekday1 = 1,
Eweekday2 = 2,
Eweekday3 = 3,
Eweekday4 = 4
};
typedef Eweeekday Ewd; // 重新定义枚举类型Eweekday的名字
引用
变量的引用就是一个变量的别名,变量和变量的引用代表着同一个变量。紧跟在数据类型后面的&符号就是引用的声明符号,其他情况都可以认为是取地址符号。需要注意的是,
- 引用不是一种独立的数据类型,引用只有声明,没有定义。必须先定义一个变量,之后对该变量建立一个引用。
- 声明一个引用时,必须同时对其初始化,即声明该引用代表哪一个变量。例外是将引用作为参数定义函数的时候,不需要进行初始化。
- 声明一个引用后,不能再让其作为另一个变量的引用了。
- 不能建立引用数组。
- 可以建立引用的引用,也可以建立引用的指针。
关于引用的性质,如果在程序中声明了b是变量a的引用,实际上在内存中为b开辟了一个指针型的存储单元,在其中存放变量a的地址,输出引用b时,就输出b所指向的变量a的值,相当于输出*b。引用其实就是一个指针常量,他的指向不能改变,只能指向一个指定的变量。所以,引用的本质还是指针,所有引用的功能都可以由指针实现。
int a = 5;
int& b = a; // 创建a的引用
void swap(int& a, int& b); // 定义函数的时候,不需要对引用进行初始化
int a1 = 1, a2 = 5;
int& b = a1; // 将b作为a1的引用
int&b = a2; // 错误!此时就不能将b再作为a2的引用了
int a = 3;
int&b = a;
int&c = b; // c是a的引用的引用
int* p = &b; // p是a的引用的指针
*p = 5; // 此句将a的值改为了5
c = 6; // 此句将a的值又改为了6
//不用指针,用引用的方式实现 swap 函数,功能是交换两个整形变量的值,实现如下,跟指针能达到一样的效果:
void swap(int& a, int& b)
{
int t = a;
a = b;
b = a;
}
new和delete的使用
因为局部变量的局限性,只能在其作用域内使用,(比如函数里创建了一个字符串,想通过函数返回值传出去,返回的是char*,函数就返回不出去,因为是个局部变量),这个时候就需要用new来动态分配内存。
C++ 中的 new操作符和C语言中的malloc
函数类似,如果不主动 delete掉这段申请的内存的话,它会一直存在,直到进程结束后系统会回收掉这段资源。
注意,使用new申请的是内存,因此是一个指针。
另外,还有一个更重要的new优于malloc的地方,C++中的类class,用new申请一个类对象的时候,对象申请成功之后会默认调用其构造函数,而C语言中的malloc只是会申请空间,但是不会调用对象的构造函数。
int* p = new int(5); //C++ 中使用 new 来申请一个int类型变量的内存
delete p; //删除变量
int* p = new int[5]; //使用new申请一个包含5个int元素的数组
delete [] p; //删除数组
类的声明和类的成员函数
cpp中的类是从struct发展而来的,因此,struct支持的,class都支持。
cpp中,类的声明中,如果不写public,默认成员是private的。
C语言中可以用宏来实现一些相对简单的函数,例如:
宏跟函数的区别是,在编译阶段就将宏的代码展开直接替换调用宏的地方。所以省去了函数调用的压栈、出栈等开销。所以执行效率方面要比函数高。但是宏定义写起来比较麻烦,对于比较长的,不适合用宏定义。(一般来说,对于那些需要频繁调用的小函数,可以写成宏的形式)
#define MAX_NUM(x, y) (x > y ? x, y)
在cpp的类中,可以写inline内联函数(类似于宏),将函数的代码直接嵌入到调用的地方,大大的减少了函数调用的开销,提高了效率。
class Student
{
public:
string name;
int num;
int age;
private:
char sex;
inline int max_num(int x, int y)
{
return x > y ? x : y;
}
public:
int get_max_num(int a, int b, int c)
{
int max_ab = max_num(a, b);
return max_ab > c ? max_ab : c;
}
void print_name()
{
cout << "name = " << name << endl;
}
};
注意:
-
默认情况下,在类体中直接定义/实现的函数,C++会自动的将其作为inline内联函数来处理,所以上面代码中的:max_num、get_max_num、print_name 函数都会被看成是 inline 内联函数。而在类体外部定义的函数(指的是在类内声明,在类外定义的函数)C++则会将其作为普通的类的成员函数来处理。如果想把类外定义的函数作为类的内联函数,需要加上在函数最前面加上
inline
作显式声明。 -
在函数的声明或函数的定义两者之一作inline声明即可。值得注意的是,如果在类体外定义inline函数,则必须将类定义和成员函数的定义都放在同一个头文件中(或者写在同一个源文件中),否则编译时无法进行置换(将函数代码的拷贝嵌入到函数调用点)。但是这样做,不利于类的接口与类的实现分离,不利于信息隐蔽。虽然程序的执行效率提高了,但从软件工程质量的角度来看,这样做并不是好的办法。只有在类外定义的成员函数规模很小而调用频率较高时,才将此成员函数指定为内置函数。
this指针
C++语言中每个对象所占用的存储空间只是该对象的数据成员所占用的存储空间,而不包括成员函数代码所占用的存储空间。因此,在调用成员函数的时候,函数是通过一个特殊的指针(this
指针)来区分不同的对象。
有些时候在类里需要用到this,比如类某成员函数的参数和类的某个数据成员的变量名一样,这个时候需要加上this来区分。
class Student
{
public:
char name[50];
int num;
int age;
public:
void set_age(int age)
{
this->age = age;
};
};
类的构造函数
构造函数(constructors)是一种特殊的成员函数,与其他成员函数不同,不需要用户来主动调用它,构造函数会在对象被建立时自动被调用的。
构造函数的注意事项:
- 构造函数的名字必须与类名同名,不能随意命名;
- 构造函数不具有任何类型,不返回任何值,连 void 都不是;
- 构造函数不需要用户调用,也不应该被用户调用,它是对象建立的时候自动被系统调用,用来初始化刚刚建立的对象的;
- 如果用户没有定义自己的类的构造函数,那么系统会自动生成一个默认的构造函数,只不过该构造函数的函数体是空的,也就是什么都不做。
不带参数的构造函数叫Default Constructors,能够为每个参数提供默认值。
class Triangular {
Triangular(); // default constructors
}
Triangular::Triangular() {
//default constructor
_length = 1;
_beg_pos =1;
_next = 1;
}
其他的自定义的constructor类似:
Triangular::Triangular(int len, int bp)
{
// _length和_beg_pos都必须>=1
_length = len > 0 ? len : 1;
_beg_pos = bp > 0 ? bp : 1;
_next = _beg_pos - 1;
}
另一种初始化的语法是member initialization list(成员初始化表)。
Member initialization list紧接在参数表最后的冒号后面,是一个以逗号为分割的列表,其中,欲赋值给class中member的数值被置于member名称后面的小括号中:
Triangular::Triangular(const Triangular &rhs)
: _length(rhs._length),
_beg_pos(rhs._beg_pos),
_next(rhs._beg_pos-1)
{ } // Yes, it's empty.
一旦class的对象被定义出来,编译器便自动根据获得的参数,挑选出应被调用的constructor,例如:
Triangular t;
Triangular t2(10, 3);
Triangular t3 = 8; //这是在调用constructor,而不是assignment operator
Triangular t5(); // 不能这样写,这样会把t5当成一个函数!
函数的重载
C++允许同一函数名定义多个函数,这些函数的参数类型和个数可以不相同,而且至少要有一个不相同(如果都相同的话就会报重复定义的链接错误了)。使一个函数名可以多用。
函数重载的要求:
重载函数的参数个数、参数类型、参数顺序 三者中必须至少有一种不同(不然会产生调用疑惑)。函数的返回值类型可以相同也可以不同。
// 函数重载的例子,这里是求两个不同类型数字的最大值
int max_num(int a, int b);
bool max_num(int a, int b);
float max_num(float a, float b);
函数的默认参数
为了使函数更加灵活,cpp支持默认参数。比如下面的例子:
函数默认参数的注意事项:
- 在函数声明的时候指定,如果没有函数的声明则可以放在函数的定义中,但是声明和定义只能选一个;
- 从第一个有默认值的参数开始,后面的所有参数必须都有默认值才行;
- 调用的时候如果要自定义非第一个默认值的参数,那么前面所有的参数都要明确的写上默认值才行;
- 从使用角度来说函数的默认值比重载更方便,从函数内部实现角度来说比函数的重载更复杂。
void get_min_max(int src[], int arr_len, int* max_v = NULL, int* min_v = NULL); // 函数的声明
void get_min_max(src, arr_len, NULL, min_v); // 如果不想输入第三个参数,就输入其默认的值
void get_min_max(src, arr_len, NULL); // 如果不想输入最后一个参数,可以直接省略
类的析构函数
析构函数也是一个在类中跟构造函数类似的特殊功能的成员函数。只不过它的作用是与构造函数相反,是在对象的生命周期结束的时候会被自动调用的。在C++中析构函数的名字跟类名相同,并且前面带上一个取反的符号~,表达的意思也就是跟构造函数的过程相反。
析构函数不返回任何值,没有函数类型,也没有任何函数的参数。所以析构函数不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。
以下几种情况会自动调用析构函数:
- 如果在一个函数中定义了一个局部变量的对象,那么当这个函数执行结束时也就是该变量对象生命周期结束的时候,所以析构函数会被自动调用;
- 全局变量或者static类型的变量,他们的生命周期一般是在程序退出的时候,这时候该对象的析构函数才会被调用;
- 如果是用new操作符动态的创建了一个对象,只有当用delete进行释放该对象的时候,析构函数才会被调用;
对象的赋值与复制
用等号=
给对象赋值,只是对对象的成员变量的赋值,对于成员函数来说不理会,也不做赋值处理,因为本身每个类的成员函数都一样,不需要赋值。
有些成员是不能用=
赋值的,比如成员变量是其他类对象的时候。
对象的赋值有时候是危险的,比如在对象里使用了new动态分配内存,且在析构函数里delete了内存,这个时候,在一个局部空间中对对象进行赋值,程序会崩溃。
Student zhangsan = {"zhangsan", 1002, 20};
Student lisi(zhangsan); // 对象的复制
Student wangwu = lisi; // 对象的赋值
对象的复制,用到了一个特殊的构造函数,名为拷贝构造函数,用法如下:
class Student
{
public:
char name[50];
int num;
int age;
Student(char* pname, int t_num, int t_age) :num(t_num), age(t_age)
{
strcpy(name, pname);
}
Student(Student& stud)
{
strcpy(name, stud.name);
num = stud.num;
age = stud.age;
}
};
拷贝构造函数的特点:
- 也是构造函数,所以函数名字就是类名字,且无返回值;
- 参数是一般是本类的对象的引用;
- 如果类没有提供拷贝构造函数,那么编译器会默认生成一个拷贝构造函数,作用比较单一,只是简单的将成员变量的值进行复制过去而已。
尽管编译器会默认生成一个默认的拷贝构造函数,但是由于类的成员变量中有一些是无法进行赋值,因此需要自定义实现拷贝构造函数。
类的静态成员
类的静态成员变量:
- 所有对象都可以直接访问这个静态成员变量,而且值是一样的;
- 静态成员变量在内存中只占用一份存储空间;
- 静态成员变量的值对于所有对象来说都是一样的。如果其中一个对象调用函数将其改变了,那么其他成员在访问这个静态成员变量的时候的值都是改变之后的;
- 只要在类中定义了类的静态成员变量,那么就占用存储空间了,不管有没有定义这个类的对象,因为静态成员变量不属于任何类对象,而是属于该类;
- 静态数据成员需要在类外进行声明或声明并初始化,否则在使用的时候会报链接的错误;
- 类的静态成员在定义的时候需要加 static,在类外声明的时候不需要加 static 关键字;
- 不能用构造函数的参数初始化表的形式对静态成员进行初始化操作;
- 静态数据成员既可以通过对象名引用,也可以通过类名来直接引用;
- public 公有类型的静态数据成员,可以被类的对象引用,也可以直接用类名来引用。但如果静态数据成员被定义成private私有的,那么通过对象或者类名来引用都是不可以的,必须通过类的public类型的静态成员函数引用。
类的静态成员函数:
- 类的静态成员函数也不属于该类的对象,而是属于这个类
- 类的静态成员函数可以通过类对象调用,也可以通过类名调用;
- 类的静态成员函数是属于类,不属于任何对象,所以静态成员函数中没有this指针,也就无法访问本类的非静态成员变量,因为不知道是哪个对象的。
- 类的静态成员函数可以直接引用类的静态成员变量,因为他们的作用域相同,都是属于该类的。
运算符重载(Operator Overloading)
同一个运算符可以有不同的功能。
运算符重载格式:
返回值类型 operator 运算符名称 (形参列表){
//TODO:
}
在类中重载运算符:
class complex{
public:
complex();
complex(double real, double imag);
public:
//声明运算符重载
complex operator+(const complex &A) const;
void display() const;
private:
double m_real; //实部
double m_imag; //虚部
};
complex::complex(): m_real(0.0), m_imag(0.0){ }
complex::complex(double real, double imag): m_real(real), m_imag(imag){ }
//实现运算符重载
complex complex::operator+(const complex &A) const{
return complex(this->m_real + A.m_real, this->m_imag + A.m_imag);
}
void complex::display() const{
cout<<m_real<<" + "<<m_imag<<"i"<<endl;
}
int main(){
complex c1(4.3, 5.8);
complex c2(2.4, 3.7);
complex c3;
c3 = c1 + c2;
c3.display();
return 0;
}
运算符重载函数不仅可以作为类的成员函数,还可以作为全局函数。
在全局范围内重载运算符:
可以看到,运算符重载函数不是 complex 类的成员函数,但是却用到了 complex 类的 private 成员变量,所以必须在 complex 类中将该函数声明为友元函数。
#include <iostream>
using namespace std;
class complex{
public:
complex();
complex(double real, double imag);
public:
void display() const;
//声明为友元函数
friend complex operator+(const complex &A, const complex &B);
private:
double m_real;
double m_imag;
};
complex operator+(const complex &A, const complex &B);
complex::complex(): m_real(0.0), m_imag(0.0){ }
complex::complex(double real, double imag): m_real(real), m_imag(imag){ }
void complex::display() const{
cout<<m_real<<" + "<<m_imag<<"i"<<endl;
}
//在全局范围内重载+
complex operator+(const complex &A, const complex &B){
complex C;
C.m_real = A.m_real + B.m_real;
C.m_imag = A.m_imag + B.m_imag;
return C;
}
int main(){
complex c1(4.3, 5.8);
complex c2(2.4, 3.7);
complex c3;
c3 = c1 + c2;
c3.display();
return 0;
}
网友评论