C++中的双冒号::名解析(Scope Resolution Operator)! |
文章目录
- C++中的双冒号名解析(Scope Resolution Operator),也称作作用域运算符,用于指明一个标识符的作用域。双冒号一般用于以下几种情况:
1. 访问命名空间中的成员
- 在C++中,命名空间是将全局作用域分隔为更小的区域,以避免命名冲突的一种机制。可以使用双冒号来访问命名空间中的成员,例如:
namespace ns {
int a;
void foo() {}
}
int main() {
ns::a = 1; // 使用双冒号访问命名空间中的 a
ns::foo(); // 使用双冒号访问命名空间中的 foo 函数
}
2. 访问类中的静态成员
- 在 C++ 中,可以使用类名和双冒号来访问类中的静态成员,例如:
// C++ program to show that :: can be used to access static
// members when there is a local variable with same name
#include<iostream>
using namespace std;
class Test
{
static int x;
public:
static int y;
// Local parameter 'a' hides class member
// 'a', but we can access it using ::
void func(int x)
{
// We can access class's static variable
// even if there is a local variable
cout << "Value of static x is " << Test::x;
cout << "\nValue of local x is " << x;
}
};
// In C++, static members must be explicitly defined
// like this
int Test::x = 1;
int Test::y = 2;
int main()
{
Test obj;
int x = 3 ;
obj.func(x);
cout << "\nTest::y = " << Test::y;
return 0;
}
3. 嵌套类访问
- 如果另一个类中存在一个类,我们可以使用嵌套类使用作用域运算符来引用嵌套的类,例如:
// Use of scope resolution class inside another class.
#include <iostream>
using namespace std;
class outside {
public:
int x;
class inside {
public:
int x;
static int y;
int foo();
};
};
int outside::inside::y = 5;
int main() {
outside A;
outside::inside B;
}
- 下面这种情况和下面
std::vector<int>::iterator
的情况一样。
// Use of scope resolution class inside another class.
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "constructor" << endl; }
};
class B {
public:
typedef A B_A;
};
int main(int argc, char const *argv[]) {
B::B_A a;
return 0;
}
// constructor
- c++中的标准库vector中,
std::vector<int>::iterator it;
给出解释:在这个例子中,std是标准命名空间,vector是std中的一个类,int是vector类的模板参数之一,iterator是vector类中的一个类型定义(type definition)。因此,std::vector::iterator表示vector类的迭代器类型,它的别名为iterator。- 也就是说:当一个类内部定义了一个嵌套类型(nested type),比如 std::vector 内部定义了 iterator 类型,我们可以使用 :: 来访问这个类型。
- 源码中iterator是这样定义的,
typedef __gnu_cxx::__normal_iterator<pointer, vector> iterator;
这里的 __gnu_cxx::__normal_iterator 实际上是一个迭代器类,用于在容器中遍历元素。pointer 表示指向元素的指针类型,而 vector 则表示容器类型。- typedef 是 C++ 中的一个关键字,用于给一个已有的数据类型起一个新的名称。它的作用是让程序员可以用一个简短、易懂的名字来代替一个复杂、冗长的数据类型名称,从而提高程序的可读性和可维护性。
# typedef <原类型> <新类型名>;
typedef int myInt;
myInt a = 5;
- 在 C++11 标准中,也可以使用 using 来定义类型别名,如下,这样定义的效果与上述 typedef 的效果相同。
using myInt = int;
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = vec.begin(); // 使用 :: 访问嵌套类型 iterator
std::cout << *it << std::endl; // 输出 1
return 0;
}
4. 在类之外定义函数
// C++ program to show that scope resolution operator :: is used
// to define a function outside a class
#include<iostream>
using namespace std;
class A
{
public:
// Only declaration
void fun();
};
// Definition outside class using ::
void A::fun()
{
cout << "fun() called";
}
int main()
{
A a;
a.fun();
return 0;
}
5. 当存在具有相同名称的局部变量时,要访问全局变量
// C++ program to show that we can access a global variable
// using scope resolution operator :: when there is a local
// variable with same name
#include<iostream>
using namespace std;
int x; // Global x
int main()
{
int x = 10; // Local x
cout << "Value of global x is " << ::x;
cout << "\nValue of local x is " << x;
return 0;
}
6. C++模板参数的自动推导
7. C++类成员函数后面加const
- 如果类的成员函数后面加了const关键字,说明这个函数是不能改变类中的成员变量的。 如果在编写该函数会修改类中的成员变量,编译时会出错,并且也提高了程序的可读性,当我们看到函数后面有const的话就知道这个函数是不会修改类中数据的。mutable (mjuːtəb(ə))意思是可变的。
8. C++类构造函数后前加explicit(ɪkˈsplɪsɪt显式的)
- C++中的explicit关键字 只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的,而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。
- 那么显示声明的构造函数和隐式声明的有什么区别呢? 我们来看下面的例子:
#include <cstring>
#include <iostream>
class CString // 没有使用explicit关键字的类声明, 即默认为隐式声明
{
public:
CString(int size)
{
_size = size; // string的预设大小
_pstr = (char *)malloc(size + 1); // 分配string的内存
memset(_pstr, 0, size + 1);
}
CString(const char *p)
{
int size = strlen(p);
_pstr = (char *)malloc(size + 1); // 分配string的内存
strcpy(_pstr, p); // 复制字符串
_size = strlen(_pstr);
}
// 析构函数这里不讨论, 省略...
protected:
char *_pstr;
int _size;
};
int main(int argc, char const *argv[])
{
CString string1(24); // 这样是OK的, 为CString预分配24字节的大小的内存
CString string2 = 10; // 这样是OK的, 为CString预分配10字节的大小的内存
// CString string3; // 这样是不行的, 因为没有默认构造函数, 错误为: “CString”: 没有合适的默认构造函数可用
CString string4("aaaa"); // 这样是OK的
CString string5 = "bbb"; // 这样也是OK的, 调用的是CString(const char *p)
CString string6 = 'c'; // 这样也是OK的, 其实调用的是CString(int size), 且size等于'c'的ascii码
string1 = 2; // 这样也是OK的, 为CString预分配2字节的大小的内存
string2 = 3; // 这样也是OK的, 为CString预分配3字节的大小的内存
// string3 = string1; // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用
return 0;
}
- 上面的代码中,
CxString string2 = 10;
这句为什么是可以的呢?- 在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象. 也就是说 “CxString string2 = 10;” 这段代码, 编译器自动将整型转换为CString类对象, 实际上等同于下面的操作:
CxString string2(10);
// 或
CxString temp(10);
CxString string2 = temp;
- 但是, 上面的代码中的_size代表的是字符串内存分配的大小, 那么调用的第二句 “CxString string2 = 10;” 和第六句 “CxString string6 = ‘c’;” 就显得不伦不类, 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. 我们把上面的代码修改一下, 如下:
- explicit关键字的作用就是防止类构造函数的隐式自动转换
9. C++函数指针和指针函数
- 首先我们能先来看这两种概念的写法:
- 函数指针:
int (*f) (int x,int y);
函数指针本质是一个指针,其指向一个函数。- 指针函数:
int *f (int x,int y);
指针函数本质是一个函数,其返回值为指针。
- 函数指针:
#include <stdio.h>
#include <stdlib.h>
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
/*这是一个函数指针,因为()的优先级大于*的有限级,但是(*f)排在前边,所示这是一个指针,
函数指针就是一个指针,他和变量指针一样,指向的都是一个地址,例如变量指针(也就是我们常说的指针)指向的是这个变量的地址
而函数指针,指向的是这个函数的地址
*/
int (*f)(int x, int y); // 定义了一个函数指针
int main()
{
f = &add; // 因为f是一个函数指针,也就是一个指针,那么就是一个地址,现在取函数add的地址赋给f,也就是函数f的功能等同于函数add的功能
printf("1+2= %d\n", (*f)(1, 2));
f = add; // 这里&add,add都可以都是首地址
printf("1+2= %d\n", (*f)(1, 2));
f = ⊂
printf("1-2= %d\n", (*f)(1, 2));
f = sub;
printf("1-2= %d\n", (*f)(1, 2));
}
// 1 + 2 = 3
// 1 - 2 = -1
- 指针函数:
#include <iostream>
typedef struct {
int a;
int b;
} Data;
// 指针函数
Data *f(int a, int b)
{
Data *data = new Data;
data->a = a;
data->b = b;
return data;
}
int main(int argc, const char *argv[])
{
Data *myData = f(4, 5);
std::cout << "f(4,5)= " << myData->a << myData->b;
return 0;
}
10. C++类模板例子(简单队列)
- my_queue.h:
#include <cstdlib>
// 空队列异常类
class EQueueEmpty {};
// 队列类项前置声明
template <typename T>
class JuQueueItem;
/// @brief 队列类模板, 这里我们使用一个单向链表保存队列中的元素
/// @tparam T
template <typename T>
class JuQueue {
public:
JuQueue() : _head(NULL), _tail(NULL) {}
virtual ~JuQueue();
virtual void Enter(const T &item); // 队尾入队
virtual T Leave(); // 对头出队
bool IsEmpty() const { return _head == NULL; } // 队列是否为空
private:
JuQueueItem<T> *_head, *_tail; // 头指针和尾指针, 指向队列的第一个元素和最后一个元素。
};
/// @brief 队列项类模板的定义,单向链表结构
/// @tparam T
template <typename T>
class JuQueueItem {
friend class JuQueue<T>; // 友元类,JuQueue<T>就是JuQueueltem<T>的友元, 你存了这种型的数据, 那么那个队列本身就是它的友元, 以方便它访问这里边结点中的数据
public:
JuQueueItem(const T &item) : _item(item), next(NULL) {}
private:
T _item;
JuQueueItem<T> *next;
};
- my_queue.cpp:
#include "my_queue.h"
#include <iostream>
// 队列类模板析构函数
template <typename T>
JuQueue<T>::~JuQueue() {
while (!IsEmpty()) {
Leave(); // 队列非空的时候,一个接着一个地删除队列中的全部的元素
}
}
// 入队
template <typename T>
void JuQueue<T>::Enter(const T &item) { // 把项插入到队列中,插入到队尾
JuQueueItem<T> *p = new JuQueueItem<T>(item); // 定义一个指针, 指向JuQueueItem<T>这样的型, new一个对象。
if (IsEmpty()) {
_head = _tail = p;
} else {
_tail->next = p;
_tail = p;
}
}
// 出队
template <typename T>
T JuQueue<T>::Leave() {
if (IsEmpty())
throw EQueueEmpty();
JuQueueItem<T> *p = _head; // 找到对头元素
T _retval = p->_item; // 要出队列的元素
_head = _head->next; //
delete p; // 销毁队列的首节点
return _retval;
}
int main() {
// 第一种方式int类型
JuQueue<int> *p = new JuQueue<int>;
for (int i = 0; i < 10; i++) {
p->Enter(i);
}
while (p != NULL)
std::cout << p->Leave() << std::endl;
// 第二种整数的地址放在队列里, 也就是说我存的不再是那个整数
/*
对于整数来讲 意义好像不是那么大, 但是如果你是用这个队列来存储一个复杂的数据结构, 那么存指针显然要比存数据要经济得多, 至少不需要频繁的拷贝和赋值
*/
int *r = new int(10), *q = new int(20);
JuQueue<int *> *t = new JuQueue<int *>;
t->Enter(r);
t->Enter(q);
int *s = t->Leave();
std::cout << *s << std::endl;
return 0;
}
11. C++智能指针(memory头文件)
- 智能指针有好几种:shared_ptr,unique_ptr,weak_ptr
- shared_ptr:共享对象的所有权,但性能略差。
- unique_ptr:独占对象的所有权,由于没有引用计数,因此性能较好。
- weak_ptr:配合shared_ptr,解决循环引用的问题
- shared_ptr共享指针:会记录有多少个共享指针指向同一个物体,当这个记录数字降为0的时候,程序会自动释放这个物体,省去我们手动delete的烦恼。shared_ptr的这个方法就叫引用计数,顾名思义引用计数的意思就是数一数有多少个共享指针指向某个物体
#include <iostream>
#include <memory>
int main(int argc, char const *argv[])
{
std::shared_ptr<int> p1;
p1 = std::make_shared<int>(100); // 方法1: make_shared初始化, 更推荐使用这一种, 因为make_shared的效率更高, c++17之前的编译器更安全
std::shared_ptr<int> p2{new int(100)}; // 方法2: 使用new初始化
std::cout << *p2 << std::endl;
return 0;
}
#include <iostream>
#include <memory>
/// @brief 创建一个球类,使用共享指针管理Ball的创建与销毀
class Ball
{
public:
Ball() { std::cout << "A ball appears." << std::endl; }
~Ball() { std::cout << "A ball disappears." << std::endl; }
void Bounce() { std::cout << "A ball jumps." << std::endl; }
};
int main(int argc, char const *argv[])
{
std::shared_ptr<Ball> p = std::make_shared<Ball>();
std::cout << p.use_count() << std::endl; // 多少个指针指向物体
std::shared_ptr<Ball> p2 = p;
std::cout << p.use_count() << " " << p2.use_count() << std::endl;
std::shared_ptr<Ball> p3 = p;
std::cout << p.use_count() << " " << p2.use_count() << " " << p3.use_count() << std::endl;
// p.reset(); // 重置,不在指向原来的物体, 3个shared_ptr都重置之后, 就没有shared_ptr指向开头的ball, 所以他就自动释了
// p2.reset();
// p3.reset();
return 0;
}
- 那么你可以调用sharedL_ptr的get方法来获取一个裸指针指向当前的资源,需要注意的是,如果你有一块资源同时有裸指针和共享指针指向它,那么当所有的共享指针都被摧毀,但是裸指针仍然存在的时候,这个底下的资源仍然会被释放。这个时候,你再用裸指针去访问那块资源就变成了未定义的行为。因此一个良好的习惯就是用共享指针的时候避免跟裸指针混用。
Ball *rp = p.get();
- c++是否应避免使用普通指针,而使用智能指针(包括shared,unique,weak)?https://www.zhihu.com/question/319277442/answer/1517987598
12. C++之Lambda表达式(匿名函数)
- C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。
- Lambda 的语法形式如下:
[capture](parameters) mutable ->return-type{statement}
13. C++11中=delete的巧妙用法
- C++11中,当我们定义一个类的成员函数时,如果后面使用"=delete"去修饰,那么就表示这个函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则就会出错。
- 在C++11之前,当我们希望一个类不能被拷贝,就会 把构造函数定义为private,但是在C++11里就不需要这样做了,只需要在构造函数后面加上=delete 来修饰下就可以了。
14. C++静态库和动态库
- 详细的可以参考-C++静态库与动态库:https://www.cnblogs.com/skynet/p/3372855.html
- 苏丙榅Linux 静态库和动态库:https://subingwen.cn/linux/library/
- Linux通过ar工具将目标文件打包成.a静态库文件的命令解析
- Linux创建动态库(.so)
15. C++之extern关键字
- extern 关键字用于声明一个在其他文件中定义的全局变量或函数。下面是一个使用 extern 的实例:假设有两个源文件:main.cpp 和 helper.cpp,它们位于同一个项目中。helper.cpp 中定义了一个全局变量 g_count,而 main.cpp 需要访问该变量。
// --------------- helper.cpp
#include <iostream>
int g_count = 0; // 定义全局变量
void helper_func() {
std::cout << "This is a helper function." << std::endl;
}
// --------------- main.cpp
#include <iostream>
extern int g_count; // 声明全局变量
extern void helper_func(); // 声明函数
int main() {
g_count++;
std::cout << "The value of g_count is: " << g_count << std::endl;
helper_func();
return 0;
}
- 在 main.cpp 中,使用 extern int g_count 声明全局变量 g_count,表示它是在其他文件中定义的。然后,在 main 函数中,可以直接使用该变量,例如对其进行加一操作。同样,使用 extern void helper_func() 声明函数 helper_func,可以在 main 函数中调用该函数。
- 需要注意的是,在编译和链接时,需要同时编译 main.cpp 和 helper.cpp,并将它们链接在一起,否则会出现链接错误。
16. C++之typedef的四种用法
- typedef理解总结一句话:“加不加typedef,类型是一样的“,这句话可以这样理解:
- 没加typedef之前如果是个数组,那么加typedef之后就是数组类型;
- 没加typedef之前如果是个函数指针,那么加typedef之后就是函数指针类型;
- 没加typedef之前如果是个指针数组,那么加typedef之后就是指针数组类型
typedef char TA[5]; // 定义数组类型
typedef char *TB[5]; // 定义指针数组类型,PA定义的变量为含5个char*指针元素的数组(指针数组类型)
typedef char *(TC[5]); // 指针数组类型,因为[]的结合优先级最高,所以加不加()没啥区别,TC等价于TB
typedef char (*TD)[5]; // 数组指针类型
(1) 给已定义的变量类型起个别名
// ①
typedef unsigned char uint8_t; // uint8_t就是unsigned char的别名,这是最基础的用法
// ②
struct __person {
char name[20];
uint8_t age;
uint8_t height;
};
typedef __person person_t;
// 以上两段代码也可合并为一段,如下:
typedef struct __person {
char name[20];
uint8_t age;
uint8_t height;
} person_t;
// 作用是给struct __person起了个别名person_t,这种这种用法也很基础
(2) 定义函数指针类型
- 我们首先来看一下如何定义函数指针变量,然后再看如何定义函数指针类型
// 1、定义函数指针变量: 定义了一个函数指针变量pFunc,它可以指向这样的函数:返回值为int,形参为char*、int
int (*pFunc)(char *frame, int len);
// 定义了5个函数指针变量:pFunc[0]、pFunc[1]···,它们都可以指向这样的函数:返回值为int*,形参为int
int *(*pFunc[5])(int len);
// 2、 定义函数指针类型:定义函数指针类型,必须使用typedef,方法就是,在“定义函数指针变量”加上typedef。
typedef int (*pFunc_t)(char *frame, int len); //定义了一个类型函数指针pFunc_t
#include <iostream>
using namespace std;
typedef int (*pFunc_t)(char frame, int len); // 定义了一个类型pFunc_t
int read_voltage(char data, int len)
{
int voltage = 0;
cout << data << endl;
return voltage;
}
int main(int argc, char const *argv[])
{
pFunc_t pHandler = read_voltage; // 使用类型pFunc_t来定义函数指针变量
pHandler('z', 1);
return 0;
}
(3) 定义数组指针类型
- 这个问题还是分两步,先看如何定义数组指针变量,再看如何定义数组指针类型
- 1. 定义数组指针变量
// 1、定义数组指针变量
int(*pArr)[5]; //定义了一个数组指针变量pArr,pArr可以指向一个int [5]的一维数组
char(*pArr)[4][5]; //定义了一个数组指针变量pArr,pArr可以指向一个char[4][5]的二维数组
// 第一行定义了一个名为pArr的变量,它是一个指向含有5个int元素的一维数组的指针类型(即int(*)[5])
int(*pArr)[5]; // pArr是一个指向含5个int元素的一维数组的指针变量
int a[5] = {1, 2, 3, 4, 5};
int b[6] = {1, 2, 3, 4, 5, 6};
pArr = &a; // 完全合法,无警告
pArr = a; // 发生编译警告,赋值时类型不匹配:a的类型为int(*),而pArr的类型为int(*)[5]
pArr = &a[0]; // 发生编译警告,赋值时类型不匹配:a的类型为int(*),而pArr的类型为int(*)[5]
pArr = &b; // 发生编译警告,赋值时类型不匹配:&b的类型为int(*)[6],而pArr的类型为int(*)[5]
pArr = (int(*)[5]) & b; // 类型强制转换为int(*)[5],完全合法,无警告
- 2. 定义数组指针类型
- 如同上面定义函数指针类型的方法,直接在前面加typedef即可,例如
#include <iostream>
using namespace std;
typedef int (*pArr_t)[5]; // 定义了一个指针类型pArr_t,该类型的指针可以指向含5个int元素的一维数组
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int b[6] = {1, 2, 3, 4, 5, 6};
pArr_t pA; // 定义数组指针变量pA
pA = &a; // 完全合法,无警告
pA = (pArr_t)&b; // 类型强制转换为pArr_t,完全合法,无警告
return 0;
}
(4) 定义数组类型
- 如果我们想声明一个含5个int元素的一维数组,一般会这么写:
int a[5];
- 如果我们想声明多个含5个int元素的一维数组,一般会这么写:
int a1[5], a2[5], a3[5]···,或者 a[N][5]
- 可见,对于定义多个一维数组,写起来略显复杂,这时,我们就应该把数组定义为一个类型,例如:
#include <iostream>
using namespace std;
typedef int arr_t[5]; // 定义了一个数组类型arr_t,该类型的变量是个数组
int main(void)
{
arr_t d; // d是个数组,这一行等价于: int d[5];
arr_t b1, b2, b3; // b1, b2, b3都是数组
d[0] = 1;
d[1] = 2;
d[4] = 134;
// d[5] = 253; // 编译警告:下标越界
}
(5) typedef目的简化表达式
- 定义指针:
int *p1, *p2; // 传统做法
int *p1, p2; // 这样是定义一个指针一个整型变量
// 利用typedef定义指针:
typedef int *pint;
pint p1, p2; // 利用typedef定义指针的方法,因为pint经过typedef后变成一个int*类型的别名
- 定义结构体:
typedef struct book{
int x;
int y;
}BOOK,*PBOOK,**PPBOOK;
//BOOK:就是struct book的别名;
//PBOOK:就是struct book*的别名
//PPBOOK:就是struct book**的别名
- 如何对函数指针进行初始化:
// 定义:
typedef int*(*PFUNC)(int,char*)
//在不看typedef时,int*(*PFUNC)(int,char*)
//中的PFUNC就是函数的变量名,所以在typedef的作用下
//PFUNC就就变成int*(*变量名)(int,char*)类型
//PFUNC pfunc; pfunc是指针变量,就相当于定义了一个int*(*pfunc)(int,char*)的函数
- 例如:PFUNC pfunc://定义一个函数指针;
步骤:- 1.先定义一个函数指针,该指针返回一个空值;
- 2.然后指针变量指向空指针变量名(因为一个函数本身就是一个地址,所以可以直接指向一个函数,但参数和返回值的类型要一致)
#include <iostream>
using namespace std;
int *call(int n, char *str)
{
return NULL;
}
typedef int *(*PFUNC)(int, char *);
int main()
{
PFUNC pfunc;
pfunc = call; // 初始化函数指针
pfunc(1, "hello"); // 传入参数
}