日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達者為師

網(wǎng)站首頁 編程語言 正文

C++如何去除cpp文件的注釋詳解_C 語言

作者:「已注銷」 ? 更新時間: 2022-11-19 編程語言

問題:讀取一個cpp文件,去除其中所有的單行注釋(//)和多行注釋(/**/),將去除注釋后的內(nèi)容寫入一個新文件。

注意:

不能去除字符串中的任何字符,如 "asdf//gh/**/k" 需要原樣輸出

能夠識別轉(zhuǎn)義的雙引號(\")和非轉(zhuǎn)義的雙引號("),如:

  • '\"'(單引號中的轉(zhuǎn)義雙引號字符)
  • "asdf\""?(字符串中的轉(zhuǎn)義雙引號不能表示字符串結(jié)束)
  • \"aaa"bcdef"?(同理,轉(zhuǎn)義雙引號不能表示字符串開始)

命令行參數(shù)為:

  • 參數(shù)1:原文件名
  • 參數(shù)2:新文件名

一、文件流

引入<fstream>頭文件,新建ifstream和ofstream對象,如果不能打開文件,則結(jié)束整個函數(shù)。?

#include <iostream>
#include <fstream>
 
using namespace std;
 
bool stripComment(string infile,string outfile){
    ifstream ifs;
    ifs.open(infile,ios::in);
    if(!ifs.is_open()){
        cout<<"fail to read!"<<endl;
        return true;
    }
 
    ofstream ofs;
    ofs.open(outfile,ios::out|ios::trunc);//寫文件,如果已經(jīng)存在同名的文件,則刪除它創(chuàng)建新文件
    if(!ofs.is_open()){
        cout<<"fail to write!"<<endl;
        return true;
    }
 
    ifs.close();
    ofs.close();
 }
 
int main(int argc,char** argv){
    return 0;
}

二、具體邏輯

1.如何循環(huán)讀入字符

這是最關(guān)鍵的一步。我們利用ifstream對象的get()方法無條件地讀取文件流中的一個字符。什么叫無條件?這是與>>運算符相區(qū)分的,>>會自動跳過所有空白字符,也就是說,它讀不到空格、\t、\n,這不符合我們的需求。?

同時,ifstream對象有一個putback()方法,它就是將一個字符重新放回文件流的末尾,下一次get(),我們還會讀到它:這有時候很有用。

get(char c) 會把文件流的末尾一個字符取出,放到char型變量c中。這個函數(shù)的返回值可以被轉(zhuǎn)換成一個布爾值,表示流的狀態(tài):如果流狀態(tài)正常,則返回true;如果流狀態(tài)不正常(bad,fail,讀到文件尾符號),返回false。

char temp1{};
 while(ifs.get(temp1)){
    ofs<<temp1;
 }

最簡單的情況是,利用get()循環(huán)讀入字符并存入temp1變量中,再把temp1寫入新文件中。最終我們會得到與原文件一樣的副本。

2.處理單行和多行注釋

這兩種東西有同一個特點:都是以 / 開頭的。所以我們讀取到 / 這個字符時,就要小心一點:它是不是意味著我讀到了一個注釋?

我們可以在此基礎(chǔ)上再讀一個字符,存入另一個字符變量temp2中,看看它是什么情況:

  • / :一定讀到了一個單行注釋
  • * :一定讀到了一個多行注釋
  • 其他字符:剛剛讀到的 / 只是一個普通的正斜杠

對于第三種情況,?我們這樣善后:

  • 立刻輸出temp1(/)
  • 把temp2放回文件流中等待下一次讀取,因為它可能有用(比如,是個雙引號)
  • 跳轉(zhuǎn)到一開始的while循環(huán)

?對于第一種情況:

  • 一直向后讀字符,直到讀到一個換行符\n
  • 將這個換行符放回文件流
  • 跳轉(zhuǎn)到一開始的while循環(huán)

第二種情況最麻煩,我們必須找一個辦法確定什么時候多行注釋才能結(jié)束。C++的語法規(guī)定, /* 與最近的 */ 之間是多行注釋,那么我們只需找最近的 */ 即可。

  1. while循環(huán)一直向后讀字符,直到找到一個 *
  2. 再讀一個字符,放入字符變量temp3中。
    1. temp3是一個 / ,恭喜!我們已經(jīng)找到多行注釋的完整范圍,只要簡單地break掉當(dāng)前的while循環(huán),再跳轉(zhuǎn)到開始的while循環(huán)就可以。不用輸出任何東西。
    2. 不好,temp3不是 / ,這意味著我們沒有找到多行注釋的終止處。此時應(yīng)該將temp3放回文件流,返回步驟1:這是很必要的,如果temp3正好是一個* ,它之后恰好是一個 / 呢?我們不能丟棄任何“可能有用”的字符!
while(ifs.get(temp1)){
        //處理 "http://" 或 "/**/"
        if(temp1=='/'){
 
            checkStatus=true;
            ifs.get(temp2);//再讀一個字符
 
            if(temp2=='/'){//處理"http://"
 
                checkStatus=false;
                while(ifs.get(temp2)){//一直向后讀字符,直到讀到一個換行符\n
                    if(temp2=='\n') break;
                }
                ifs.putback('\n');//將換行符放回文件流
                continue;//跳轉(zhuǎn)到一開始的while循環(huán)
 
            }else if(temp2=='*'){//處理 "/**/"
 
                checkStatus=true;
                while(ifs.get(temp3)){
                    if(temp3=='*'){//找到一個*
                        ifs.get(temp4);
                        if(temp4=='/'){//找到一個/
                            checkStatus=false;
                            break;
                        }else{//沒找到/
                            ifs.putback(temp4);
                            continue;
                        }
                    }
                }
                continue;
            }else{//只讀到了一個 /
                checkStatus=false;
                ifs.putback(temp2);//把temp2放回文件流中等待下一次讀取
            }
        }
        ofs<<temp1;
    }

3.注意字符串

我們在2中的操作會破壞字符串中的一些內(nèi)容,比如:

處理前:"asdf//gh/**/k"

處理后(//被當(dāng)成了單行注釋):"asdf

我們的需求是保留字符串原封不動輸出。

因此,當(dāng)temp1沒有讀到 / 時(即沒有進入處理注釋的邏輯),temp1讀到了一個 " ,我們就要小心了:后面的內(nèi)容是字符串,原樣輸出即可。

  • 立刻輸出temp1。
  • 循環(huán)讀入后面的字符,存入temp2中,立即輸出temp2。
  • 檢查剛剛輸出的temp2是不是 " ,如果是,結(jié)束當(dāng)前循環(huán)并跳轉(zhuǎn)到一開始的while循環(huán)
    while(ifs.get(temp1)){
        //處理 "http://" 或 "/**/"
        //省略...
 
        //處理字符串
        if(temp1=='\"'){
            ofs<<temp1;//立刻輸出第一個雙引號
            checkStatus=true;
            while(ifs.get(temp2)){
                ofs<<temp2;//立刻輸出temp2
 
                if(temp2=='\"'){//temp2是結(jié)尾的雙引號
                    checkStatus=false;
                    break;
                }
            }
        continue;//返回一開始的while循環(huán)
        }
        ofs<<temp1;
    }

4.注意轉(zhuǎn)義雙引號

以上的兩塊邏輯能解決一般問題,但在以下的例子中會出錯:

char c='\"';
const char* p1 = "sdjksd\"\\d//fj/*kdhjk\"dsfjl*/dks";

int main()
{
    cout << "http://The number of queens (4 -- 12) :// " ;
}

最后的結(jié)果是:

char c='\"';
const char* p1 = "sdjksd\"\\d//fj/*kdhjk\"dsfjl*/dks";

int main()
{
    cout << "
}

為什么呢?其實是轉(zhuǎn)義雙引號(\")在作怪,因為我們目前的邏輯將轉(zhuǎn)義雙引號與表示字符串開頭與結(jié)尾的普通雙引號混為一談,導(dǎo)致字符串的邊界出現(xiàn)混亂。

以下紅色部分表示程序所認為的字符串,藍色部分表示程序認為是注釋的地方():

可以看出這個問題挺嚴重的,首要之急就是區(qū)分轉(zhuǎn)義雙引號和普通雙引號。

C++的文件流是逐個字符讀取的,也就是說,\" 在get()時,第一次會得到 \ ,第二次會得到 "

另一個有用的信息就是,C++的字符串邊界永遠是普通雙引號,而不是轉(zhuǎn)義雙引號。

1>防止轉(zhuǎn)義雙引號作為字符串開頭

  • 當(dāng)沒有進入處理注釋的邏輯時,讀到了一個 \
  • 立刻輸出 \ 本身
  • 讀取下一個字符存入temp2中
  • 立刻輸出temp2
  • 跳轉(zhuǎn)到一開始的while循環(huán)

為什么這樣做呢?分析一下,因為 \ 與它后面的字符已經(jīng)被轉(zhuǎn)義(它們是一個整體),\ 后面的字符不能被認為是代碼語法的一部分。比如:\ 之后的 " 不能被認為是普通的雙引號,\ 之后的 / 也不能被認為是一個代表注釋開頭的 /。反斜杠后面的字符從語法上來說應(yīng)該直接輸出。

注:處理注釋時不需這種邏輯,注釋里的 \ 沒有什么特殊的意義,和普通字符地位一樣。它不能影響后面那個字符的含義。

/*aaaaa\*/

仍然表示一個多行注釋

2>防止轉(zhuǎn)義雙引號作為字符串結(jié)尾

要在字符串的輸出邏輯中增加檢查機制。

  • 剛剛輸出的字符(temp2)如果是一個 \
  • 讀取下一個字符并直接輸出
  • 繼續(xù)執(zhí)行當(dāng)前的while輸出循環(huán),直到temp2是一個雙引號為止?
    while(ifs.get(temp1)){
        //處理 "http://" 或 "/**/"
        //...省略
 
        //防止\"作為字符串開頭
        if(temp1=='\\'){
            checkStatus=true;
            ofs<<temp1;//直接輸出\
            //如果\后面有字符,直接輸出\后面的字符
            if(ifs.get(temp2)){
                ofs<<temp2;
                checkStatus=false;
            }
            continue;//跳轉(zhuǎn)到一開始的while循環(huán)
        }
 
        //處理字符串
        if(temp1=='\"'){
            ofs<<temp1;
            checkStatus=true;
            while(ifs.get(temp2)){
                ofs<<temp2;
 
                //如果剛剛輸出的是一個\
                if(temp2=='\\'){
                    if(ifs.get(temp3)) ofs<<temp3;//直接輸出后一個字符
                    continue;//繼續(xù)當(dāng)前的while循環(huán)
                }
 
                if(temp2=='\"'){//輸出了一個普通雙引號,表示字符串結(jié)束
                    checkStatus=false;
                    break;
                }
            }
        continue;
        }
        ofs<<temp1;
    }

5.增加簡單的查錯功能

當(dāng)我們發(fā)現(xiàn)了 /* 或 " 時,這意味著我們開始進行注釋邊界的確定或字符串邊界的確定。當(dāng)發(fā)現(xiàn)了一個 \ 之后,\ 后面理應(yīng)有一個字符。

如果我們:

  • 找到了 "?卻始終未找到對應(yīng)的 "
  • 找到了 /* 卻始終沒有找到 */
  • 找到了 \ 后面卻無字符

這三種情況意味著原文件的語法一定有問題,我們用一個變量checkStatus表示。當(dāng)開始進行"尋找"過程,checkStatus被設(shè)為true。當(dāng)尋找結(jié)束后,變量的值被設(shè)為false。如果文件流結(jié)束,此變量依然為true,那么應(yīng)提醒用戶,文件的語法有問題。

完整代碼如下:

#include <iostream>
#include <fstream>
 
using namespace std;
 
bool stripComment(string infile,string outfile){
    ifstream ifs;
    ifs.open(infile,ios::in);
    if(!ifs.is_open()){
        cout<<"fail to read!"<<endl;
        return true;
    }
 
    ofstream ofs;
    ofs.open(outfile,ios::out|ios::trunc);
    if(!ofs.is_open()){
        cout<<"fail to write!"<<endl;
        return true;
    }
 
    bool checkStatus=false;
    char temp1{};
    char temp2{};
 
    char temp3{};
 
    while(ifs.get(temp1)){
 
 
        //handle "http://" OR "/**/"
        if(temp1=='/'){
 
            checkStatus=true;
            ifs.get(temp2);
 
 
            if(temp2=='/'){//handle "http://"
 
                checkStatus=false;
                while(ifs.get(temp2)){
                    if(temp2=='\n') break;
                }
                ifs.putback('\n');
                continue;
 
            }else if(temp2=='*'){//handle "/**/"
 
                checkStatus=true;
                while(ifs.get(temp2)){
                    if(temp2=='*'){
                        ifs.get(temp3);
                        if(temp3=='/'){
                            checkStatus=false;
                            break;
                        }else{
                            ifs.putback(temp3);
                            continue;
                        }
                    }
                }
                continue;
            }else{//only read a '/'
                checkStatus=false;
                ifs.putback(temp2);
            }
        }
 
        //handle possible '\"'
        if(temp1=='\\'){
            checkStatus=true;
            ofs<<temp1;
            //output char after '\'
            if(ifs.get(temp2)){
                ofs<<temp2;
                checkStatus=false;
            }
            continue;
        }
 
        //handle true ""
        if(temp1=='\"'){
            ofs<<temp1;
            checkStatus=true;
            while(ifs.get(temp2)){
                ofs<<temp2;
                //handle things after '\'
                if(temp2=='\\'){
                    if(ifs.get(temp3)) ofs<<temp3;
                    continue;
                }
                if(temp2=='\"'){
                    checkStatus=false;
                    break;
                }
            }
        continue;
        }
 
        ofs<<temp1;
 
    }
 
    ifs.close();
    ofs.close();
    return checkStatus;
}
 
int main(int argc,char** argv){
 
 
    if(argc!=3){//檢查是否輸入了兩個文件名
        cout<<"argument invalid!"<<endl;
        return -1;
    }
 
    bool res=stripComment(argv[1],argv[2]);//去除注釋
 
    if(res==true){//文件語法有錯
        cout<<"maybe wrong."<<endl;
    }
    return 0;
}

三、正則實現(xiàn)(Java)

這是題外話,但其實正則表達式也能實現(xiàn)這一功能。

由于C++的正則似乎不支持后發(fā)斷言,所以使用了Java的正則類。

主要思路:

  1. 讀取待去除注釋的文本,存入一個字符串中。
  2. 創(chuàng)建兩個正則表達式,一個匹配注釋和字符串,另一個只能匹配注釋。
  3. 對該字符串循環(huán)匹配第一個正則表達式,得到結(jié)果str。
  4. 若str符合第二個正則表達式,則記下str在原字符串中的起始下標和結(jié)束下標,將二者都存入ArrayList對象中。
  5. 匹配結(jié)束后,遍歷原字符串,如果字符下標位于”存儲下來的注釋區(qū)域的起始和結(jié)束下標“之間,就不輸出這些字符。
package project1100;
 
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class mainTest {
    public static void main(String[] args) {
 
        String str2=
                "const char *qq=\"/**/\";\n" +
                "const char *pp=/**\"\\\"007*/\"/**/\";\n" +
                "const char *rr=/**'\\'007*/\"/**/\"\"http://\";";
        
        //匹配注釋和字符串
        Pattern pattern1=Pattern.compile("(/((/.*)|(\\*[\\S\\s]*?\\*/)))|((?<!\\\\)(\"([\\S\\s]*?)[^\\\\]\"))");
        //只能匹配注釋
        Pattern pattern2=Pattern.compile("(/((/.*)|(\\*[\\S\\s]*?\\*/)))");
        //用于存儲注釋區(qū)域下標范圍的容器
        ArrayList<Integer> arrayList=new ArrayList<Integer>();
 
        Matcher matcher1=pattern1.matcher(str2);
 
        //匹配注釋和字符串
        while(matcher1.find()){
            //是注釋,下標存入容器中
            if(pattern2.matcher(matcher1.group(0)).matches()) {
                arrayList.add(matcher1.start(0));
                arrayList.add(matcher1.end(0));
 
            }
        }
 
        //選擇輸出
        label:for(int i=0;i<str2.length();i++){
            for(int j=0;j<arrayList.size()/2;j++){
                if(i>=arrayList.get(j*2)&&i<arrayList.get(j*2+1)){
                    continue label;
                }
            }
            System.out.print(str2.charAt(i));
        }
        /*結(jié)果
            const char *qq="/**/";
            const char *pp="/**/";
            const char *rr="/**/""http://";
        */
    }
}

?圖解:

?可能仍有疏漏,請各位大佬指正。

結(jié)語:

去除代碼中的注釋,看似簡單,實則要考慮諸多情況。這是一個極其考驗思維細致度的問題,希望大家都能考慮到每一種情況,找到自己最好理解的思路。

原文鏈接:https://blog.csdn.net/m0_62505136/article/details/121548141

欄目分類
最近更新